diff --git a/.gitignore b/.gitignore index 40c3db7..01362f6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,12 +5,14 @@ *.tar.gz *.box *.log -data/minceraft/*.log -data/minceraft/ultimmc.cfg +data/minceraft/instances/1.21.4/.minecraft/authlib-injector.log data/minceraft/instances/1.21.4/.minecraft/logs -data/minceraft/instances/1.21.4/.minecraft/*.log -data/minceraft/meta*/ -data/minceraft/assets/ +data/minecraft/* +data/minceraft/*/ +!data/minecraft/instances +!data/minecraft/ultimmc.cfg.def +!data/minecraft/instances/ +!data/minecraft/run_mine.sh output/ build/ xbps-cache diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0aa1017 --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +PHONY=build iso ultimmc clean + +build: iso + +clean: + rm -rf output + cp -r data/minceraft data/minceraft_tmp + rm -rf data/minceraft + mkdir data/minceraft + cp data/minceraft_tmp/ultimmc.cfg.def data/minceraft + cp -r data/minceraft_tmp/instances data/minceraft + cp data/minceraft_tmp/run_mine.sh data/minceraft + rm -rf data/minceraft_tmp + rm -rf ultimmc/build + +ultimmc: data/minceraft/UltimMC + +data/minceraft/UltimMC: + mkdir -p ultimmc/build + cd ultimmc/build && cmake \ + -DCMAKE_C_COMPILER=/usr/bin/gcc \ + -DCMAKE_CXX_COMPILER=/usr/bin/g++ \ + -DCMAKE_BUILD_TYPE=Release \ + -DLauncher_NOTIFICATION_URL:STRING=https://files.multimc.org/notifications.json \ + -DCMAKE_INSTALL_PREFIX:PATH=../ \ + -DLauncher_UPDATER_BASE=https://files.multimc.org/update/ \ + -DLauncher_PASTE_EE_API_KEY:STRING=utLvciUouSURFzfjPxLBf5W4ISsUX4pwBDF7N1AfZ \ + -DLauncher_ANALYTICS_ID:STRING=UA-87731965-2 \ + -DLauncher_LAYOUT=lin-nodeps \ + -DLauncher_BUILD_PLATFORM=lin64 \ + -DLauncher_BUG_TRACKER_URL=https://github.com/UltimMC/Launcher/issues \ + -DLauncher_EMBED_SECRETS=On \ + .. + cd ultimmc/build && make + cp -a ultimmc/build/. data/minceraft/ + rm -rf ultimmc/build + chmod 777 data/minceraft -R + echo "CLOSE MINECRAFT WHEN ASSETS ARE LOADED!!" + cp data/minceraft/ultimmc.cfg.def data/minceraft/ultimmc.cfg + cd data/minceraft && ./UltimMC -o -n Steve -l 1.21.4 + cp data/minceraft/ultimmc.cfg.def data/minceraft/ultimmc.cfg + +iso: data/minceraft/UltimMC + sudo ./mkmine.sh diff --git a/README.md b/README.md index 1e95b1d..73bc38b 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,34 @@ # minceraftOS OS that uses Minceraft as Desktop Environment. \ -Now it only launches minceraft and does nothing. - -## Download iso file - -You can get iso file of latest version here: [Latest release](https://github.com/MeexReay/minceraftOS/releases/latest) \ -But if you want to get all latest unstable changes and fixes, you need to build iso file yourself: [How to make iso file](https://github.com/MeexReay/minceraftOS#how-to-make-iso-file) +Now it only starts minceraft and do nothing. ## How to make iso file You need to do this from Void Linux (because the script needs XBPS to build)! -Load minceraft assets at first: +At first, compile ultimmc and load minceraft assets: ``` -./data/minceraft/UltimMC -l 1.21.4 # close minecraft when it's loaded -cp data/minceraft/ultimmc.cfg.def data/minceraft/ultimmc.cfg +make ultimmc + +# it will start game to load assets, +# you just need to close the window when it will open ``` Use `mkmine.sh` script to make ISO file. \ Result will be in `output/` directory. \ -Script compiles it only for x86_64, but I think it's not really hard to make it compile for any other architectures +Script compiles it only for x86_64, but I think it's not really hard to make it compile for any other architecture ``` -sudo ./mkmine.sh # idk why it needs sudo, please pr if you know how to remove it +sudo ./mkmine.sh +sudo make iso # the same but also prepares launcher + +# idk why it needs sudo, please pr if you know how to remove it ``` +Finally, you can forget all above and use just `sudo make` + ## How to burn it on disk First link in google bro diff --git a/data/minceraft/accounts.json b/data/minceraft/accounts.json deleted file mode 100644 index 7932612..0000000 --- a/data/minceraft/accounts.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "accounts": [ - { - "active": true, - "entitlement": { - "canPlayMinecraft": true, - "ownsMinecraft": true - }, - "profile": { - "capes": [ - ], - "id": "5627dd98e6be3c21b8a8e92344183641", - "name": "Steve", - "skin": { - "id": "", - "url": "", - "variant": "" - } - }, - "type": "Local", - "ygg": { - "extra": { - "clientToken": "08db5b7f52414da9acae79e23aa07e63", - "userName": "Steve" - }, - "iat": 1741787232 - } - } - ], - "formatVersion": 3 -} diff --git a/data/minceraft/bin/UltimMC b/data/minceraft/bin/UltimMC deleted file mode 100755 index a39756e..0000000 Binary files a/data/minceraft/bin/UltimMC and /dev/null differ diff --git a/data/minceraft/bin/jars/JavaCheck.jar b/data/minceraft/bin/jars/JavaCheck.jar deleted file mode 100755 index 4566498..0000000 Binary files a/data/minceraft/bin/jars/JavaCheck.jar and /dev/null differ diff --git a/data/minceraft/bin/jars/NewLaunch.jar b/data/minceraft/bin/jars/NewLaunch.jar deleted file mode 100755 index ac2937f..0000000 Binary files a/data/minceraft/bin/jars/NewLaunch.jar and /dev/null differ diff --git a/data/minceraft/bin/libLauncher_iconfix.so b/data/minceraft/bin/libLauncher_iconfix.so deleted file mode 100755 index 4b89fcb..0000000 Binary files a/data/minceraft/bin/libLauncher_iconfix.so and /dev/null differ diff --git a/data/minceraft/bin/libLauncher_nbt++.so b/data/minceraft/bin/libLauncher_nbt++.so deleted file mode 100755 index 1b2300c..0000000 Binary files a/data/minceraft/bin/libLauncher_nbt++.so and /dev/null differ diff --git a/data/minceraft/bin/libLauncher_quazip.so b/data/minceraft/bin/libLauncher_quazip.so deleted file mode 100755 index ec2fc5a..0000000 Binary files a/data/minceraft/bin/libLauncher_quazip.so and /dev/null differ diff --git a/data/minceraft/bin/libLauncher_rainbow.so b/data/minceraft/bin/libLauncher_rainbow.so deleted file mode 100755 index 3f5835e..0000000 Binary files a/data/minceraft/bin/libLauncher_rainbow.so and /dev/null differ diff --git a/data/minceraft/injectors/authlib-injector-1.2.5.jar b/data/minceraft/injectors/authlib-injector-1.2.5.jar deleted file mode 100644 index dc50e5b..0000000 Binary files a/data/minceraft/injectors/authlib-injector-1.2.5.jar and /dev/null differ diff --git a/data/minceraft/injectors/version.json b/data/minceraft/injectors/version.json deleted file mode 100644 index e534e1f..0000000 --- a/data/minceraft/injectors/version.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "build_number": 53, - "version": "1.2.5", - "release_time": "2024-02-17T17:24:35Z", - "download_url": "https://authlib-injector.yushi.moe/artifact/53/authlib-injector-1.2.5.jar", - "checksums": { - "sha256": "3bc9ebdc583b36abd2a65b626c4b9f35f21177fbf42a851606eaaea3fd42ee0f" - } -} diff --git a/data/minceraft/instances/1.21.4/.minecraft/authlib-injector.log b/data/minceraft/instances/1.21.4/.minecraft/authlib-injector.log index 012ca3b..da63eed 100755 --- a/data/minceraft/instances/1.21.4/.minecraft/authlib-injector.log +++ b/data/minceraft/instances/1.21.4/.minecraft/authlib-injector.log @@ -1,6 +1,6 @@ -Logging started at 2025-03-12T20:00:28.230448162Z +Logging started at 2025-03-14T01:16:18.022394035Z [authlib-injector] [INFO] Version: 1.2.5 -[authlib-injector] [INFO] Authentication server: http://127.0.0.1:43699 +[authlib-injector] [INFO] Authentication server: http://127.0.0.1:42059 [authlib-injector] [WARNING] You are using HTTP protocol, which is INSECURE! Please switch to HTTPS if possible. [authlib-injector] [INFO] Transformed [net.minecraft.client.main.Main] with [Main Arguments Transformer] [authlib-injector] [INFO] Transformed [com.mojang.authlib.properties.Property] with [Yggdrasil Public Key Transformer] diff --git a/data/minceraft/instances/1.21.4/.minecraft/options.txt b/data/minceraft/instances/1.21.4/.minecraft/options.txt old mode 100644 new mode 100755 diff --git a/data/minceraft/instances/1.21.4/instance.cfg b/data/minceraft/instances/1.21.4/instance.cfg index 350ed48..1e07b61 100755 --- a/data/minceraft/instances/1.21.4/instance.cfg +++ b/data/minceraft/instances/1.21.4/instance.cfg @@ -48,8 +48,8 @@ UseNativeGLFW=false UseNativeOpenAL=false WrapperCommand= iconKey=default -lastLaunchTime=1741809628094 -lastTimePlayed=94 +lastLaunchTime=1741914977888 +lastTimePlayed=89 name=1.21.4 notes= -totalTimePlayed=584 +totalTimePlayed=673 diff --git a/data/minceraft/libraries/com/fasterxml/jackson/core/jackson-annotations/2.13.4/jackson-annotations-2.13.4.jar b/data/minceraft/libraries/com/fasterxml/jackson/core/jackson-annotations/2.13.4/jackson-annotations-2.13.4.jar deleted file mode 100755 index 0c5e9c1..0000000 Binary files a/data/minceraft/libraries/com/fasterxml/jackson/core/jackson-annotations/2.13.4/jackson-annotations-2.13.4.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/fasterxml/jackson/core/jackson-core/2.13.4/jackson-core-2.13.4.jar b/data/minceraft/libraries/com/fasterxml/jackson/core/jackson-core/2.13.4/jackson-core-2.13.4.jar deleted file mode 100755 index 0cb7a37..0000000 Binary files a/data/minceraft/libraries/com/fasterxml/jackson/core/jackson-core/2.13.4/jackson-core-2.13.4.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/fasterxml/jackson/core/jackson-databind/2.13.4.2/jackson-databind-2.13.4.2.jar b/data/minceraft/libraries/com/fasterxml/jackson/core/jackson-databind/2.13.4.2/jackson-databind-2.13.4.2.jar deleted file mode 100755 index 5b653d6..0000000 Binary files a/data/minceraft/libraries/com/fasterxml/jackson/core/jackson-databind/2.13.4.2/jackson-databind-2.13.4.2.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/github/oshi/oshi-core/6.6.5/oshi-core-6.6.5.jar b/data/minceraft/libraries/com/github/oshi/oshi-core/6.6.5/oshi-core-6.6.5.jar deleted file mode 100755 index 53775a5..0000000 Binary files a/data/minceraft/libraries/com/github/oshi/oshi-core/6.6.5/oshi-core-6.6.5.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/github/stephenc/jcip/jcip-annotations/1.0-1/jcip-annotations-1.0-1.jar b/data/minceraft/libraries/com/github/stephenc/jcip/jcip-annotations/1.0-1/jcip-annotations-1.0-1.jar deleted file mode 100755 index edfda76..0000000 Binary files a/data/minceraft/libraries/com/github/stephenc/jcip/jcip-annotations/1.0-1/jcip-annotations-1.0-1.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/google/code/gson/gson/2.11.0/gson-2.11.0.jar b/data/minceraft/libraries/com/google/code/gson/gson/2.11.0/gson-2.11.0.jar deleted file mode 100755 index 18e59c8..0000000 Binary files a/data/minceraft/libraries/com/google/code/gson/gson/2.11.0/gson-2.11.0.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/google/guava/failureaccess/1.0.2/failureaccess-1.0.2.jar b/data/minceraft/libraries/com/google/guava/failureaccess/1.0.2/failureaccess-1.0.2.jar deleted file mode 100755 index d73ab80..0000000 Binary files a/data/minceraft/libraries/com/google/guava/failureaccess/1.0.2/failureaccess-1.0.2.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/google/guava/guava/33.3.1-jre/guava-33.3.1-jre.jar b/data/minceraft/libraries/com/google/guava/guava/33.3.1-jre/guava-33.3.1-jre.jar deleted file mode 100755 index 9a88e16..0000000 Binary files a/data/minceraft/libraries/com/google/guava/guava/33.3.1-jre/guava-33.3.1-jre.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/ibm/icu/icu4j/76.1/icu4j-76.1.jar b/data/minceraft/libraries/com/ibm/icu/icu4j/76.1/icu4j-76.1.jar deleted file mode 100755 index 17f5845..0000000 Binary files a/data/minceraft/libraries/com/ibm/icu/icu4j/76.1/icu4j-76.1.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/microsoft/azure/msal4j/1.17.2/msal4j-1.17.2.jar b/data/minceraft/libraries/com/microsoft/azure/msal4j/1.17.2/msal4j-1.17.2.jar deleted file mode 100755 index c2f9d39..0000000 Binary files a/data/minceraft/libraries/com/microsoft/azure/msal4j/1.17.2/msal4j-1.17.2.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/mojang/authlib/6.0.57/authlib-6.0.57.jar b/data/minceraft/libraries/com/mojang/authlib/6.0.57/authlib-6.0.57.jar deleted file mode 100755 index 8552698..0000000 Binary files a/data/minceraft/libraries/com/mojang/authlib/6.0.57/authlib-6.0.57.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/mojang/blocklist/1.0.10/blocklist-1.0.10.jar b/data/minceraft/libraries/com/mojang/blocklist/1.0.10/blocklist-1.0.10.jar deleted file mode 100755 index 4b5c6a9..0000000 Binary files a/data/minceraft/libraries/com/mojang/blocklist/1.0.10/blocklist-1.0.10.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/mojang/brigadier/1.3.10/brigadier-1.3.10.jar b/data/minceraft/libraries/com/mojang/brigadier/1.3.10/brigadier-1.3.10.jar deleted file mode 100755 index fed2379..0000000 Binary files a/data/minceraft/libraries/com/mojang/brigadier/1.3.10/brigadier-1.3.10.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/mojang/datafixerupper/8.0.16/datafixerupper-8.0.16.jar b/data/minceraft/libraries/com/mojang/datafixerupper/8.0.16/datafixerupper-8.0.16.jar deleted file mode 100755 index faa58c2..0000000 Binary files a/data/minceraft/libraries/com/mojang/datafixerupper/8.0.16/datafixerupper-8.0.16.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/mojang/jtracy/1.0.29/jtracy-1.0.29-natives-linux.jar b/data/minceraft/libraries/com/mojang/jtracy/1.0.29/jtracy-1.0.29-natives-linux.jar deleted file mode 100755 index d8edd57..0000000 Binary files a/data/minceraft/libraries/com/mojang/jtracy/1.0.29/jtracy-1.0.29-natives-linux.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/mojang/jtracy/1.0.29/jtracy-1.0.29.jar b/data/minceraft/libraries/com/mojang/jtracy/1.0.29/jtracy-1.0.29.jar deleted file mode 100755 index 012083b..0000000 Binary files a/data/minceraft/libraries/com/mojang/jtracy/1.0.29/jtracy-1.0.29.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/mojang/logging/1.5.10/logging-1.5.10.jar b/data/minceraft/libraries/com/mojang/logging/1.5.10/logging-1.5.10.jar deleted file mode 100755 index 4985959..0000000 Binary files a/data/minceraft/libraries/com/mojang/logging/1.5.10/logging-1.5.10.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/mojang/minecraft/1.21.4/minecraft-1.21.4-client.jar b/data/minceraft/libraries/com/mojang/minecraft/1.21.4/minecraft-1.21.4-client.jar deleted file mode 100755 index 776646b..0000000 Binary files a/data/minceraft/libraries/com/mojang/minecraft/1.21.4/minecraft-1.21.4-client.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/mojang/patchy/2.2.10/patchy-2.2.10.jar b/data/minceraft/libraries/com/mojang/patchy/2.2.10/patchy-2.2.10.jar deleted file mode 100755 index e9abf8e..0000000 Binary files a/data/minceraft/libraries/com/mojang/patchy/2.2.10/patchy-2.2.10.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/mojang/text2speech/1.17.9/text2speech-1.17.9.jar b/data/minceraft/libraries/com/mojang/text2speech/1.17.9/text2speech-1.17.9.jar deleted file mode 100755 index 716ceb0..0000000 Binary files a/data/minceraft/libraries/com/mojang/text2speech/1.17.9/text2speech-1.17.9.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/nimbusds/content-type/2.3/content-type-2.3.jar b/data/minceraft/libraries/com/nimbusds/content-type/2.3/content-type-2.3.jar deleted file mode 100755 index 7e88769..0000000 Binary files a/data/minceraft/libraries/com/nimbusds/content-type/2.3/content-type-2.3.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/nimbusds/lang-tag/1.7/lang-tag-1.7.jar b/data/minceraft/libraries/com/nimbusds/lang-tag/1.7/lang-tag-1.7.jar deleted file mode 100755 index c089707..0000000 Binary files a/data/minceraft/libraries/com/nimbusds/lang-tag/1.7/lang-tag-1.7.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/nimbusds/nimbus-jose-jwt/9.40/nimbus-jose-jwt-9.40.jar b/data/minceraft/libraries/com/nimbusds/nimbus-jose-jwt/9.40/nimbus-jose-jwt-9.40.jar deleted file mode 100755 index 342eee7..0000000 Binary files a/data/minceraft/libraries/com/nimbusds/nimbus-jose-jwt/9.40/nimbus-jose-jwt-9.40.jar and /dev/null differ diff --git a/data/minceraft/libraries/com/nimbusds/oauth2-oidc-sdk/11.18/oauth2-oidc-sdk-11.18.jar b/data/minceraft/libraries/com/nimbusds/oauth2-oidc-sdk/11.18/oauth2-oidc-sdk-11.18.jar deleted file mode 100755 index e372022..0000000 Binary files a/data/minceraft/libraries/com/nimbusds/oauth2-oidc-sdk/11.18/oauth2-oidc-sdk-11.18.jar and /dev/null differ diff --git a/data/minceraft/libraries/commons-codec/commons-codec/1.17.1/commons-codec-1.17.1.jar b/data/minceraft/libraries/commons-codec/commons-codec/1.17.1/commons-codec-1.17.1.jar deleted file mode 100755 index 5023670..0000000 Binary files a/data/minceraft/libraries/commons-codec/commons-codec/1.17.1/commons-codec-1.17.1.jar and /dev/null differ diff --git a/data/minceraft/libraries/commons-io/commons-io/2.17.0/commons-io-2.17.0.jar b/data/minceraft/libraries/commons-io/commons-io/2.17.0/commons-io-2.17.0.jar deleted file mode 100755 index ad00ddc..0000000 Binary files a/data/minceraft/libraries/commons-io/commons-io/2.17.0/commons-io-2.17.0.jar and /dev/null differ diff --git a/data/minceraft/libraries/commons-logging/commons-logging/1.3.4/commons-logging-1.3.4.jar b/data/minceraft/libraries/commons-logging/commons-logging/1.3.4/commons-logging-1.3.4.jar deleted file mode 100755 index b6339bb..0000000 Binary files a/data/minceraft/libraries/commons-logging/commons-logging/1.3.4/commons-logging-1.3.4.jar and /dev/null differ diff --git a/data/minceraft/libraries/io/netty/netty-buffer/4.1.115.Final/netty-buffer-4.1.115.Final.jar b/data/minceraft/libraries/io/netty/netty-buffer/4.1.115.Final/netty-buffer-4.1.115.Final.jar deleted file mode 100755 index 5372981..0000000 Binary files a/data/minceraft/libraries/io/netty/netty-buffer/4.1.115.Final/netty-buffer-4.1.115.Final.jar and /dev/null differ diff --git a/data/minceraft/libraries/io/netty/netty-codec/4.1.115.Final/netty-codec-4.1.115.Final.jar b/data/minceraft/libraries/io/netty/netty-codec/4.1.115.Final/netty-codec-4.1.115.Final.jar deleted file mode 100755 index 342629f..0000000 Binary files a/data/minceraft/libraries/io/netty/netty-codec/4.1.115.Final/netty-codec-4.1.115.Final.jar and /dev/null differ diff --git a/data/minceraft/libraries/io/netty/netty-common/4.1.115.Final/netty-common-4.1.115.Final.jar b/data/minceraft/libraries/io/netty/netty-common/4.1.115.Final/netty-common-4.1.115.Final.jar deleted file mode 100755 index 6b610ea..0000000 Binary files a/data/minceraft/libraries/io/netty/netty-common/4.1.115.Final/netty-common-4.1.115.Final.jar and /dev/null differ diff --git a/data/minceraft/libraries/io/netty/netty-handler/4.1.115.Final/netty-handler-4.1.115.Final.jar b/data/minceraft/libraries/io/netty/netty-handler/4.1.115.Final/netty-handler-4.1.115.Final.jar deleted file mode 100755 index 30a7a35..0000000 Binary files a/data/minceraft/libraries/io/netty/netty-handler/4.1.115.Final/netty-handler-4.1.115.Final.jar and /dev/null differ diff --git a/data/minceraft/libraries/io/netty/netty-resolver/4.1.115.Final/netty-resolver-4.1.115.Final.jar b/data/minceraft/libraries/io/netty/netty-resolver/4.1.115.Final/netty-resolver-4.1.115.Final.jar deleted file mode 100755 index 63d1b3e..0000000 Binary files a/data/minceraft/libraries/io/netty/netty-resolver/4.1.115.Final/netty-resolver-4.1.115.Final.jar and /dev/null differ diff --git a/data/minceraft/libraries/io/netty/netty-transport-classes-epoll/4.1.115.Final/netty-transport-classes-epoll-4.1.115.Final.jar b/data/minceraft/libraries/io/netty/netty-transport-classes-epoll/4.1.115.Final/netty-transport-classes-epoll-4.1.115.Final.jar deleted file mode 100755 index 84e06bc..0000000 Binary files a/data/minceraft/libraries/io/netty/netty-transport-classes-epoll/4.1.115.Final/netty-transport-classes-epoll-4.1.115.Final.jar and /dev/null differ diff --git a/data/minceraft/libraries/io/netty/netty-transport-native-epoll/4.1.115.Final/netty-transport-native-epoll-4.1.115.Final-linux-aarch_64.jar b/data/minceraft/libraries/io/netty/netty-transport-native-epoll/4.1.115.Final/netty-transport-native-epoll-4.1.115.Final-linux-aarch_64.jar deleted file mode 100755 index 5d5f754..0000000 Binary files a/data/minceraft/libraries/io/netty/netty-transport-native-epoll/4.1.115.Final/netty-transport-native-epoll-4.1.115.Final-linux-aarch_64.jar and /dev/null differ diff --git a/data/minceraft/libraries/io/netty/netty-transport-native-epoll/4.1.115.Final/netty-transport-native-epoll-4.1.115.Final-linux-x86_64.jar b/data/minceraft/libraries/io/netty/netty-transport-native-epoll/4.1.115.Final/netty-transport-native-epoll-4.1.115.Final-linux-x86_64.jar deleted file mode 100755 index ce021e5..0000000 Binary files a/data/minceraft/libraries/io/netty/netty-transport-native-epoll/4.1.115.Final/netty-transport-native-epoll-4.1.115.Final-linux-x86_64.jar and /dev/null differ diff --git a/data/minceraft/libraries/io/netty/netty-transport-native-unix-common/4.1.115.Final/netty-transport-native-unix-common-4.1.115.Final.jar b/data/minceraft/libraries/io/netty/netty-transport-native-unix-common/4.1.115.Final/netty-transport-native-unix-common-4.1.115.Final.jar deleted file mode 100755 index 78db923..0000000 Binary files a/data/minceraft/libraries/io/netty/netty-transport-native-unix-common/4.1.115.Final/netty-transport-native-unix-common-4.1.115.Final.jar and /dev/null differ diff --git a/data/minceraft/libraries/io/netty/netty-transport/4.1.115.Final/netty-transport-4.1.115.Final.jar b/data/minceraft/libraries/io/netty/netty-transport/4.1.115.Final/netty-transport-4.1.115.Final.jar deleted file mode 100755 index 3a64746..0000000 Binary files a/data/minceraft/libraries/io/netty/netty-transport/4.1.115.Final/netty-transport-4.1.115.Final.jar and /dev/null differ diff --git a/data/minceraft/libraries/it/unimi/dsi/fastutil/8.5.15/fastutil-8.5.15.jar b/data/minceraft/libraries/it/unimi/dsi/fastutil/8.5.15/fastutil-8.5.15.jar deleted file mode 100755 index 2e03f48..0000000 Binary files a/data/minceraft/libraries/it/unimi/dsi/fastutil/8.5.15/fastutil-8.5.15.jar and /dev/null differ diff --git a/data/minceraft/libraries/net/java/dev/jna/jna-platform/5.15.0/jna-platform-5.15.0.jar b/data/minceraft/libraries/net/java/dev/jna/jna-platform/5.15.0/jna-platform-5.15.0.jar deleted file mode 100755 index 645b692..0000000 Binary files a/data/minceraft/libraries/net/java/dev/jna/jna-platform/5.15.0/jna-platform-5.15.0.jar and /dev/null differ diff --git a/data/minceraft/libraries/net/java/dev/jna/jna/5.15.0/jna-5.15.0.jar b/data/minceraft/libraries/net/java/dev/jna/jna/5.15.0/jna-5.15.0.jar deleted file mode 100755 index a216935..0000000 Binary files a/data/minceraft/libraries/net/java/dev/jna/jna/5.15.0/jna-5.15.0.jar and /dev/null differ diff --git a/data/minceraft/libraries/net/minidev/accessors-smart/2.5.1/accessors-smart-2.5.1.jar b/data/minceraft/libraries/net/minidev/accessors-smart/2.5.1/accessors-smart-2.5.1.jar deleted file mode 100755 index de9487c..0000000 Binary files a/data/minceraft/libraries/net/minidev/accessors-smart/2.5.1/accessors-smart-2.5.1.jar and /dev/null differ diff --git a/data/minceraft/libraries/net/minidev/json-smart/2.5.1/json-smart-2.5.1.jar b/data/minceraft/libraries/net/minidev/json-smart/2.5.1/json-smart-2.5.1.jar deleted file mode 100755 index 2842d99..0000000 Binary files a/data/minceraft/libraries/net/minidev/json-smart/2.5.1/json-smart-2.5.1.jar and /dev/null differ diff --git a/data/minceraft/libraries/net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar b/data/minceraft/libraries/net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar deleted file mode 100755 index 317b2b0..0000000 Binary files a/data/minceraft/libraries/net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/apache/commons/commons-compress/1.27.1/commons-compress-1.27.1.jar b/data/minceraft/libraries/org/apache/commons/commons-compress/1.27.1/commons-compress-1.27.1.jar deleted file mode 100755 index 1bea2d9..0000000 Binary files a/data/minceraft/libraries/org/apache/commons/commons-compress/1.27.1/commons-compress-1.27.1.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.jar b/data/minceraft/libraries/org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.jar deleted file mode 100755 index f6486b4..0000000 Binary files a/data/minceraft/libraries/org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14.jar b/data/minceraft/libraries/org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14.jar deleted file mode 100755 index 2bb7c07..0000000 Binary files a/data/minceraft/libraries/org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/apache/httpcomponents/httpcore/4.4.16/httpcore-4.4.16.jar b/data/minceraft/libraries/org/apache/httpcomponents/httpcore/4.4.16/httpcore-4.4.16.jar deleted file mode 100755 index f0bdebe..0000000 Binary files a/data/minceraft/libraries/org/apache/httpcomponents/httpcore/4.4.16/httpcore-4.4.16.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/apache/logging/log4j/log4j-api/2.24.1/log4j-api-2.24.1.jar b/data/minceraft/libraries/org/apache/logging/log4j/log4j-api/2.24.1/log4j-api-2.24.1.jar deleted file mode 100755 index f9d5b43..0000000 Binary files a/data/minceraft/libraries/org/apache/logging/log4j/log4j-api/2.24.1/log4j-api-2.24.1.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/apache/logging/log4j/log4j-core/2.24.1/log4j-core-2.24.1.jar b/data/minceraft/libraries/org/apache/logging/log4j/log4j-core/2.24.1/log4j-core-2.24.1.jar deleted file mode 100755 index ea8eefa..0000000 Binary files a/data/minceraft/libraries/org/apache/logging/log4j/log4j-core/2.24.1/log4j-core-2.24.1.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/apache/logging/log4j/log4j-slf4j2-impl/2.24.1/log4j-slf4j2-impl-2.24.1.jar b/data/minceraft/libraries/org/apache/logging/log4j/log4j-slf4j2-impl/2.24.1/log4j-slf4j2-impl-2.24.1.jar deleted file mode 100755 index 0a914b9..0000000 Binary files a/data/minceraft/libraries/org/apache/logging/log4j/log4j-slf4j2-impl/2.24.1/log4j-slf4j2-impl-2.24.1.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/jcraft/jorbis/0.0.17/jorbis-0.0.17.jar b/data/minceraft/libraries/org/jcraft/jorbis/0.0.17/jorbis-0.0.17.jar deleted file mode 100755 index e58a6aa..0000000 Binary files a/data/minceraft/libraries/org/jcraft/jorbis/0.0.17/jorbis-0.0.17.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/joml/joml/1.10.8/joml-1.10.8.jar b/data/minceraft/libraries/org/joml/joml/1.10.8/joml-1.10.8.jar deleted file mode 100755 index ddcad59..0000000 Binary files a/data/minceraft/libraries/org/joml/joml/1.10.8/joml-1.10.8.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl-freetype/3.3.3/lwjgl-freetype-3.3.3-natives-linux.jar b/data/minceraft/libraries/org/lwjgl/lwjgl-freetype/3.3.3/lwjgl-freetype-3.3.3-natives-linux.jar deleted file mode 100755 index caa2984..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl-freetype/3.3.3/lwjgl-freetype-3.3.3-natives-linux.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl-freetype/3.3.3/lwjgl-freetype-3.3.3.jar b/data/minceraft/libraries/org/lwjgl/lwjgl-freetype/3.3.3/lwjgl-freetype-3.3.3.jar deleted file mode 100755 index 9fcdfa9..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl-freetype/3.3.3/lwjgl-freetype-3.3.3.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3-natives-linux.jar b/data/minceraft/libraries/org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3-natives-linux.jar deleted file mode 100755 index 28db301..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3-natives-linux.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3.jar b/data/minceraft/libraries/org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3.jar deleted file mode 100755 index 791fe06..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3-natives-linux.jar b/data/minceraft/libraries/org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3-natives-linux.jar deleted file mode 100755 index c72f93a..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3-natives-linux.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3.jar b/data/minceraft/libraries/org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3.jar deleted file mode 100755 index 6881c26..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3-natives-linux.jar b/data/minceraft/libraries/org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3-natives-linux.jar deleted file mode 100755 index 46252b4..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3-natives-linux.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3.jar b/data/minceraft/libraries/org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3.jar deleted file mode 100755 index 2a60334..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3-natives-linux.jar b/data/minceraft/libraries/org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3-natives-linux.jar deleted file mode 100755 index b4198d5..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3-natives-linux.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3.jar b/data/minceraft/libraries/org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3.jar deleted file mode 100755 index 9905636..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3-natives-linux.jar b/data/minceraft/libraries/org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3-natives-linux.jar deleted file mode 100755 index b8ccae2..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3-natives-linux.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3.jar b/data/minceraft/libraries/org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3.jar deleted file mode 100755 index ba65bed..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl-tinyfd/3.3.3/lwjgl-tinyfd-3.3.3-natives-linux.jar b/data/minceraft/libraries/org/lwjgl/lwjgl-tinyfd/3.3.3/lwjgl-tinyfd-3.3.3-natives-linux.jar deleted file mode 100755 index df5d573..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl-tinyfd/3.3.3/lwjgl-tinyfd-3.3.3-natives-linux.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl-tinyfd/3.3.3/lwjgl-tinyfd-3.3.3.jar b/data/minceraft/libraries/org/lwjgl/lwjgl-tinyfd/3.3.3/lwjgl-tinyfd-3.3.3.jar deleted file mode 100755 index 7fbd1bf..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl-tinyfd/3.3.3/lwjgl-tinyfd-3.3.3.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3-natives-linux.jar b/data/minceraft/libraries/org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3-natives-linux.jar deleted file mode 100755 index 68018d8..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3-natives-linux.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3.jar b/data/minceraft/libraries/org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3.jar deleted file mode 100755 index 2d1fcf9..0000000 Binary files a/data/minceraft/libraries/org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/lz4/lz4-java/1.8.0/lz4-java-1.8.0.jar b/data/minceraft/libraries/org/lz4/lz4-java/1.8.0/lz4-java-1.8.0.jar deleted file mode 100755 index 89c644b..0000000 Binary files a/data/minceraft/libraries/org/lz4/lz4-java/1.8.0/lz4-java-1.8.0.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/ow2/asm/asm/9.6/asm-9.6.jar b/data/minceraft/libraries/org/ow2/asm/asm/9.6/asm-9.6.jar deleted file mode 100755 index cc1c2cd..0000000 Binary files a/data/minceraft/libraries/org/ow2/asm/asm/9.6/asm-9.6.jar and /dev/null differ diff --git a/data/minceraft/libraries/org/slf4j/slf4j-api/2.0.16/slf4j-api-2.0.16.jar b/data/minceraft/libraries/org/slf4j/slf4j-api/2.0.16/slf4j-api-2.0.16.jar deleted file mode 100755 index cbb5448..0000000 Binary files a/data/minceraft/libraries/org/slf4j/slf4j-api/2.0.16/slf4j-api-2.0.16.jar and /dev/null differ diff --git a/data/minceraft/metacache b/data/minceraft/metacache deleted file mode 100755 index 1146eb4..0000000 --- a/data/minceraft/metacache +++ /dev/null @@ -1,645 +0,0 @@ -{ - "entries": [ - { - "base": "asset_indexes", - "etag": "", - "last_changed_timestamp": 1741809420953, - "md5sum": "d5a9c66300c0d7eacd8293b62a5986a8", - "path": "19.json", - "remote_changed_timestamp": "Wed, 12 Mar 2025 12:46:52 GMT" - }, - { - "base": "injectors", - "etag": "\"65d0ebf9-537d2\"", - "last_changed_timestamp": 1741787699920, - "md5sum": "c60d3899b711537e10be33c680ebd8ae", - "path": "authlib-injector-1.2.5.jar", - "remote_changed_timestamp": "Sat, 17 Feb 2024 17:25:13 GMT" - }, - { - "base": "injectors", - "etag": "W/\"65d0ebf9-11f\"", - "last_changed_timestamp": 1741787699583, - "md5sum": "e206374eedab95eef76581f3e5c26867", - "path": "version.json", - "remote_changed_timestamp": "Sat, 17 Feb 2024 17:25:13 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC80A2DCB5773E", - "last_changed_timestamp": 1741807485215, - "md5sum": "7e982dafcf25c24b365b5088eaf8a0a6", - "path": "com/fasterxml/jackson/core/jackson-annotations/2.13.4/jackson-annotations-2.13.4.jar", - "remote_changed_timestamp": "Thu, 30 May 2024 12:20:15 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC80A2DC6C8B4B", - "last_changed_timestamp": 1741807485216, - "md5sum": "e8f064827ddb8deb06e45d20988bcaa5", - "path": "com/fasterxml/jackson/core/jackson-core/2.13.4/jackson-core-2.13.4.jar", - "remote_changed_timestamp": "Thu, 30 May 2024 12:20:15 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC80A2DC4C0872", - "last_changed_timestamp": 1741807485222, - "md5sum": "f26eab82fa1da09f8e8bc7e52fc82088", - "path": "com/fasterxml/jackson/core/jackson-databind/2.13.4.2/jackson-databind-2.13.4.2.jar", - "remote_changed_timestamp": "Thu, 30 May 2024 12:20:14 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C56DAFC7C", - "last_changed_timestamp": 1741807485225, - "md5sum": "e4e15667573ea6a967d0171289d59e9c", - "path": "com/github/oshi/oshi-core/6.6.5/oshi-core-6.6.5.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:58 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC80A2DBDE58B1", - "last_changed_timestamp": 1741807485225, - "md5sum": "d62dbfa8789378457ada685e2f614846", - "path": "com/github/stephenc/jcip/jcip-annotations/1.0-1/jcip-annotations-1.0-1.jar", - "remote_changed_timestamp": "Thu, 30 May 2024 12:20:14 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C584D3738", - "last_changed_timestamp": 1741807485227, - "md5sum": "0c69b9199d3a4e6c34dc03619ff7feee", - "path": "com/google/code/gson/gson/2.11.0/gson-2.11.0.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:06:01 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C58333CC5", - "last_changed_timestamp": 1741807485227, - "md5sum": "3f75955b49b6758fd6d1e1bd9bf777b3", - "path": "com/google/guava/failureaccess/1.0.2/failureaccess-1.0.2.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:06:01 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C57E1334F", - "last_changed_timestamp": 1741807485237, - "md5sum": "7b7d80d99af4181db55b00dad50a91bb", - "path": "com/google/guava/guava/33.3.1-jre/guava-33.3.1-jre.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:06:00 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C569016F6", - "last_changed_timestamp": 1741807485290, - "md5sum": "0621976c76a3b05b0622aef5a4c1d981", - "path": "com/ibm/icu/icu4j/76.1/icu4j-76.1.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:58 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C59ABD8EF", - "last_changed_timestamp": 1741807485294, - "md5sum": "af37c5d9f4d839c78a103c866ea2bfe4", - "path": "com/microsoft/azure/msal4j/1.17.2/msal4j-1.17.2.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:06:03 GMT" - }, - { - "base": "libraries", - "etag": "0x8DCF8C600B13DA5", - "last_changed_timestamp": 1741807485294, - "md5sum": "f6c1f2b733258d1dc74533cbad0da8f6", - "path": "com/mojang/authlib/6.0.57/authlib-6.0.57.jar", - "remote_changed_timestamp": "Wed, 30 Oct 2024 09:34:07 GMT" - }, - { - "base": "libraries", - "etag": "0x8DAB7EC4634A6BF", - "last_changed_timestamp": 1741807485294, - "md5sum": "fc1420e3182dd32b4df9933f810ebebb", - "path": "com/mojang/blocklist/1.0.10/blocklist-1.0.10.jar", - "remote_changed_timestamp": "Thu, 27 Oct 2022 07:24:24 GMT" - }, - { - "base": "libraries", - "etag": "0x8DCB6E7544D8099", - "last_changed_timestamp": 1741807485295, - "md5sum": "a755b426eb7942bb74b46a95b02f1de4", - "path": "com/mojang/brigadier/1.3.10/brigadier-1.3.10.jar", - "remote_changed_timestamp": "Wed, 07 Aug 2024 13:46:24 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC6E7281CD0C4C", - "last_changed_timestamp": 1741807485297, - "md5sum": "d932ac637b6d83e6c45a8f269fe81e3b", - "path": "com/mojang/datafixerupper/8.0.16/datafixerupper-8.0.16.jar", - "remote_changed_timestamp": "Tue, 07 May 2024 08:48:46 GMT" - }, - { - "base": "libraries", - "etag": "0x8DCCDAC732329E6", - "last_changed_timestamp": 1741807485298, - "md5sum": "b8dc2f815666609419b80263d7a34969", - "path": "com/mojang/jtracy/1.0.29/jtracy-1.0.29-natives-linux.jar", - "remote_changed_timestamp": "Thu, 05 Sep 2024 13:12:52 GMT" - }, - { - "base": "libraries", - "etag": "0x8DCCDAC724C594D", - "last_changed_timestamp": 1741807485298, - "md5sum": "1c06cd97d006339f58085cdfd8065000", - "path": "com/mojang/jtracy/1.0.29/jtracy-1.0.29.jar", - "remote_changed_timestamp": "Thu, 05 Sep 2024 13:12:51 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD04C20161983D", - "last_changed_timestamp": 1741807485298, - "md5sum": "0b7da24cea4e840b91c13ed08657a6d3", - "path": "com/mojang/logging/1.5.10/logging-1.5.10.jar", - "remote_changed_timestamp": "Thu, 14 Nov 2024 15:35:44 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD1384AF4AC9E1", - "last_changed_timestamp": 1741807485397, - "md5sum": "70e2838411853210dce14bdd30769458", - "path": "com/mojang/minecraft/1.21.4/minecraft-1.21.4-client.jar", - "remote_changed_timestamp": "Tue, 03 Dec 2024 10:24:35 GMT" - }, - { - "base": "libraries", - "etag": "0x8DAB7EC4DC28215", - "last_changed_timestamp": 1741807485400, - "md5sum": "ff905bf0aacf501149a13880a2d6742d", - "path": "com/mojang/patchy/2.2.10/patchy-2.2.10.jar", - "remote_changed_timestamp": "Thu, 27 Oct 2022 07:24:37 GMT" - }, - { - "base": "libraries", - "etag": "0x8DB5528647B683F", - "last_changed_timestamp": 1741807485400, - "md5sum": "f5b05e8db22e2e0668b786e11ac9d3ce", - "path": "com/mojang/text2speech/1.17.9/text2speech-1.17.9.jar", - "remote_changed_timestamp": "Mon, 15 May 2023 09:40:17 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC80A2DCECEAB3", - "last_changed_timestamp": 1741807485400, - "md5sum": "f0fc0d6be73e838863e2197c03a27c3f", - "path": "com/nimbusds/content-type/2.3/content-type-2.3.jar", - "remote_changed_timestamp": "Thu, 30 May 2024 12:20:15 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC80A2DD4E9A00", - "last_changed_timestamp": 1741807485401, - "md5sum": "31b8a4f76fdbf21f1d667f9d6618e0b2", - "path": "com/nimbusds/lang-tag/1.7/lang-tag-1.7.jar", - "remote_changed_timestamp": "Thu, 30 May 2024 12:20:16 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C596A8593", - "last_changed_timestamp": 1741807485403, - "md5sum": "42ce81c8d034f163663d23e8bbc3638d", - "path": "com/nimbusds/nimbus-jose-jwt/9.40/nimbus-jose-jwt-9.40.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:06:03 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C58FA679B", - "last_changed_timestamp": 1741807485406, - "md5sum": "7ea0239e0e285c7625964893dcba95ec", - "path": "com/nimbusds/oauth2-oidc-sdk/11.18/oauth2-oidc-sdk-11.18.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:06:02 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C5466CBEB", - "last_changed_timestamp": 1741807485407, - "md5sum": "7b3438ab4c6d91e0066d410947e43f3e", - "path": "commons-codec/commons-codec/1.17.1/commons-codec-1.17.1.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:54 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C542DACB3", - "last_changed_timestamp": 1741807485409, - "md5sum": "f6232d0e290d58bb93f74f67165bf91f", - "path": "commons-io/commons-io/2.17.0/commons-io-2.17.0.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:54 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C53E22B96", - "last_changed_timestamp": 1741807485409, - "md5sum": "e7a1e7cb6a89241ed9bfec4c25b6c645", - "path": "commons-logging/commons-logging/1.3.4/commons-logging-1.3.4.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:53 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C4EE1F06E", - "last_changed_timestamp": 1741807485411, - "md5sum": "c4ddfa85fddc7cdd84ef38c87c036010", - "path": "io/netty/netty-buffer/4.1.115.Final/netty-buffer-4.1.115.Final.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:45 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C500F0B81", - "last_changed_timestamp": 1741807485412, - "md5sum": "5391594c6f5bbdd944e3e8bcecf3d9ea", - "path": "io/netty/netty-codec/4.1.115.Final/netty-codec-4.1.115.Final.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:47 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C5054C6B8", - "last_changed_timestamp": 1741807485414, - "md5sum": "6241a4cfb9c478bbd7aa12512b90735d", - "path": "io/netty/netty-common/4.1.115.Final/netty-common-4.1.115.Final.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:47 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C5120CE15", - "last_changed_timestamp": 1741807485416, - "md5sum": "2a752fae3646b70f7bd17a2265c788ed", - "path": "io/netty/netty-handler/4.1.115.Final/netty-handler-4.1.115.Final.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:49 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C50A06EB6", - "last_changed_timestamp": 1741807485416, - "md5sum": "e133793fdcb3ea2846693f1de1d31906", - "path": "io/netty/netty-resolver/4.1.115.Final/netty-resolver-4.1.115.Final.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:48 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C4FBF9735", - "last_changed_timestamp": 1741807485417, - "md5sum": "9cbf83e1fe1dcc8c92075196f6f0f88c", - "path": "io/netty/netty-transport-classes-epoll/4.1.115.Final/netty-transport-classes-epoll-4.1.115.Final.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:47 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C4F17435B", - "last_changed_timestamp": 1741807485417, - "md5sum": "e76aa9d835b3dceb7004d644d6f4badc", - "path": "io/netty/netty-transport-native-epoll/4.1.115.Final/netty-transport-native-epoll-4.1.115.Final-linux-aarch_64.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:45 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C4F21711B", - "last_changed_timestamp": 1741807485417, - "md5sum": "1949f9a395ac49f303c99251c4710844", - "path": "io/netty/netty-transport-native-epoll/4.1.115.Final/netty-transport-native-epoll-4.1.115.Final-linux-x86_64.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:45 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C50C9736A", - "last_changed_timestamp": 1741807485418, - "md5sum": "c5714e7bc9bbd800a52d8d6c145b19e2", - "path": "io/netty/netty-transport-native-unix-common/4.1.115.Final/netty-transport-native-unix-common-4.1.115.Final.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:48 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C4F7070B4", - "last_changed_timestamp": 1741807485419, - "md5sum": "c2da3befce20eaf33fa8005e0797e03a", - "path": "io/netty/netty-transport/4.1.115.Final/netty-transport-4.1.115.Final.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:46 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C565131DC", - "last_changed_timestamp": 1741807485508, - "md5sum": "da830fa5023a010d2c2af1484d13cefc", - "path": "it/unimi/dsi/fastutil/8.5.15/fastutil-8.5.15.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:58 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C555A2C31", - "last_changed_timestamp": 1741807485513, - "md5sum": "41d91e4a13428fb79c12024cb92a4091", - "path": "net/java/dev/jna/jna-platform/5.15.0/jna-platform-5.15.0.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:56 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C5594A96F", - "last_changed_timestamp": 1741807485518, - "md5sum": "cd756a719c1892e56d9c9d424e8983bb", - "path": "net/java/dev/jna/jna/5.15.0/jna-5.15.0.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:56 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C550676F3", - "last_changed_timestamp": 1741807485518, - "md5sum": "51e60dbf9ac51f6666f0077317990944", - "path": "net/minidev/accessors-smart/2.5.1/accessors-smart-2.5.1.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:55 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C549AC0D3", - "last_changed_timestamp": 1741807485519, - "md5sum": "88a65001b616c2e7796f9263ad97bbf1", - "path": "net/minidev/json-smart/2.5.1/json-smart-2.5.1.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:55 GMT" - }, - { - "base": "libraries", - "etag": "0x8DAB7EC4420E1F8", - "last_changed_timestamp": 1741807485519, - "md5sum": "eb0d9dffe9b0eddead68fe678be76c49", - "path": "net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4.jar", - "remote_changed_timestamp": "Thu, 27 Oct 2022 07:24:20 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C52C9451A", - "last_changed_timestamp": 1741807485523, - "md5sum": "1db4bd87b0082044c6e7a6af0b977a3e", - "path": "org/apache/commons/commons-compress/1.27.1/commons-compress-1.27.1.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:52 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C5261822A", - "last_changed_timestamp": 1741807485525, - "md5sum": "7730df72b7fdff4a3a32d89a314f826a", - "path": "org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:51 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C530326C3", - "last_changed_timestamp": 1741807485528, - "md5sum": "2cb357c4b763f47e58af6cad47df6ba3", - "path": "org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:52 GMT" - }, - { - "base": "libraries", - "etag": "0x8DBA8624587D567", - "last_changed_timestamp": 1741807485529, - "md5sum": "28d2cd9bf8789fd2ec774fb88436ebd1", - "path": "org/apache/httpcomponents/httpcore/4.4.16/httpcore-4.4.16.jar", - "remote_changed_timestamp": "Tue, 29 Aug 2023 07:33:42 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C520A2785", - "last_changed_timestamp": 1741807485531, - "md5sum": "4dac8bc9a21f3503cd1a856503b6fea0", - "path": "org/apache/logging/log4j/log4j-api/2.24.1/log4j-api-2.24.1.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:50 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C519C77D6", - "last_changed_timestamp": 1741807485537, - "md5sum": "fd982d71f36250bc31528fc7e3d0807d", - "path": "org/apache/logging/log4j/log4j-core/2.24.1/log4j-core-2.24.1.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:50 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C51E6E8AE", - "last_changed_timestamp": 1741807485538, - "md5sum": "033362ddeac79ba54fdc33cc2004d445", - "path": "org/apache/logging/log4j/log4j-slf4j2-impl/2.24.1/log4j-slf4j2-impl-2.24.1.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:50 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC3A103D72187B", - "last_changed_timestamp": 1741807485538, - "md5sum": "4794379b6074b962bb4cab21bfdb8d9c", - "path": "org/jcraft/jorbis/0.0.17/jorbis-0.0.17.jar", - "remote_changed_timestamp": "Fri, 01 Mar 2024 16:54:20 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C513A7AC1", - "last_changed_timestamp": 1741807485541, - "md5sum": "58a4c6e7475f1121bfd390a819e62c98", - "path": "org/joml/joml/1.10.8/joml-1.10.8.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:49 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C44385BF", - "last_changed_timestamp": 1741807485543, - "md5sum": "7da8d8e06e7315c6c877ae5e542407b6", - "path": "org/lwjgl/lwjgl-freetype/3.3.3/lwjgl-freetype-3.3.3-natives-linux.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:50 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C4FB9761", - "last_changed_timestamp": 1741807485544, - "md5sum": "4506c5f102d10aab0dae80214579352c", - "path": "org/lwjgl/lwjgl-freetype/3.3.3/lwjgl-freetype-3.3.3.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:52 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C52734E1", - "last_changed_timestamp": 1741807485545, - "md5sum": "9b31da2f80cbbdc6d76b72c8ac2cf943", - "path": "org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3-natives-linux.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:52 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C5BF8E23", - "last_changed_timestamp": 1741807485545, - "md5sum": "41c1287c1219fd0b67e8d51879f4c812", - "path": "org/lwjgl/lwjgl-glfw/3.3.3/lwjgl-glfw-3.3.3.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:53 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C6B5B12A", - "last_changed_timestamp": 1741807485546, - "md5sum": "a3867865fad43f2ccb6b20dea405b3c8", - "path": "org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3-natives-linux.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:55 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C69FBB55", - "last_changed_timestamp": 1741807485546, - "md5sum": "a8ffc7d8a0d54981f1d7b5c2a40acf01", - "path": "org/lwjgl/lwjgl-jemalloc/3.3.3/lwjgl-jemalloc-3.3.3.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:54 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C329B354", - "last_changed_timestamp": 1741807485547, - "md5sum": "82bec56d8f88b000b29550c5946bada4", - "path": "org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3-natives-linux.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:49 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C30519A1", - "last_changed_timestamp": 1741807485547, - "md5sum": "3ae8606b16891af57eb08ed5a6f78ed8", - "path": "org/lwjgl/lwjgl-openal/3.3.3/lwjgl-openal-3.3.3.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:48 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C68A13A2", - "last_changed_timestamp": 1741807485548, - "md5sum": "de05e5c258591d07d294a9c16096a383", - "path": "org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3-natives-linux.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:54 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C5FAB9C7", - "last_changed_timestamp": 1741807485551, - "md5sum": "d5a85fe9c675ff040197e2e2dd694fc1", - "path": "org/lwjgl/lwjgl-opengl/3.3.3/lwjgl-opengl-3.3.3.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:53 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C7BFCE76", - "last_changed_timestamp": 1741807485551, - "md5sum": "23a604402c5e9527ecce65e66086308a", - "path": "org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3-natives-linux.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:56 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C74FDEE5", - "last_changed_timestamp": 1741807485552, - "md5sum": "11b824be2cd8532eb6ef063ada6a75bd", - "path": "org/lwjgl/lwjgl-stb/3.3.3/lwjgl-stb-3.3.3.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:56 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C822EEA1", - "last_changed_timestamp": 1741807485552, - "md5sum": "36797f4097de3e127cefc1ed5aa09f02", - "path": "org/lwjgl/lwjgl-tinyfd/3.3.3/lwjgl-tinyfd-3.3.3-natives-linux.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:57 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C82D9B79", - "last_changed_timestamp": 1741807485552, - "md5sum": "c687eaba9debbc609df72ab45f4e1164", - "path": "org/lwjgl/lwjgl-tinyfd/3.3.3/lwjgl-tinyfd-3.3.3.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:57 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C3F808CE", - "last_changed_timestamp": 1741807485552, - "md5sum": "be04104b73f6154ceb5fdfa651e07e84", - "path": "org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3-natives-linux.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:50 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC4CD0C40748DB", - "last_changed_timestamp": 1741807485555, - "md5sum": "d89fce0be944d8cffd1f82c6546509bf", - "path": "org/lwjgl/lwjgl/3.3.3/lwjgl-3.3.3.jar", - "remote_changed_timestamp": "Mon, 25 Mar 2024 13:37:50 GMT" - }, - { - "base": "libraries", - "etag": "0x8DC1CC20CCAC2F0", - "last_changed_timestamp": 1741807485557, - "md5sum": "936a927700aa8fc3b75d21d7571171f6", - "path": "org/lz4/lz4-java/1.8.0/lz4-java-1.8.0.jar", - "remote_changed_timestamp": "Wed, 24 Jan 2024 09:51:34 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C536B109B", - "last_changed_timestamp": 1741807485557, - "md5sum": "6f8bccf756f170d4185bb24c8c2d2020", - "path": "org/ow2/asm/asm/9.6/asm-9.6.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:53 GMT" - }, - { - "base": "libraries", - "etag": "0x8DD054C534B9E10", - "last_changed_timestamp": 1741807485558, - "md5sum": "c8de8f5d740584cb24b5652cfba8b3c4", - "path": "org/slf4j/slf4j-api/2.0.16/slf4j-api-2.0.16.jar", - "remote_changed_timestamp": "Fri, 15 Nov 2024 08:05:52 GMT" - }, - { - "base": "meta", - "etag": "W/\"67d1dabf-6b3\"", - "last_changed_timestamp": 1741809417677, - "md5sum": "cf2ef84d8b941ed397e73ad3f99b1515", - "path": "index.json", - "remote_changed_timestamp": "Wed, 12 Mar 2025 19:04:31 GMT" - }, - { - "base": "meta", - "etag": "W/\"67d1dabf-10be2\"", - "last_changed_timestamp": 1741809418082, - "md5sum": "b2cbeddaca7ad3c3219dfbe6cead381e", - "path": "net.minecraft/1.21.4.json", - "remote_changed_timestamp": "Wed, 12 Mar 2025 19:04:31 GMT" - }, - { - "base": "meta", - "etag": "W/\"67d1b06d-4f79c\"", - "last_changed_timestamp": 1741796189155, - "md5sum": "947e142002eecb37abae388519809f17", - "path": "net.minecraft/index.json", - "remote_changed_timestamp": "Wed, 12 Mar 2025 16:03:57 GMT" - }, - { - "base": "root", - "etag": "\"550e6bb5-356\"", - "last_changed_timestamp": 1741787183638, - "md5sum": "a8a8094545267c76ad51fe276b0e9e8f", - "path": "notifications.json", - "remote_changed_timestamp": "Sun, 22 Mar 2015 07:13:57 GMT" - }, - { - "base": "translations", - "etag": "\"678d1f1a-3e3b\"", - "last_changed_timestamp": 1741807485558, - "md5sum": "e9a6cb5c7168458327f5ea6c5f0b6ebc", - "path": "index_v2.json", - "remote_changed_timestamp": "Sun, 19 Jan 2025 15:49:46 GMT" - }, - { - "base": "translations", - "etag": "\"678d1f1a-22df3\"", - "last_changed_timestamp": 1741807485559, - "md5sum": "8dd856913c4c78006bc4c5ddafba720b", - "path": "mmc_ru.qm", - "remote_changed_timestamp": "Sun, 19 Jan 2025 15:49:46 GMT" - } - ], - "version": "1" -} diff --git a/data/minceraft/themes/custom/theme.json b/data/minceraft/themes/custom/theme.json deleted file mode 100644 index fc6cbd7..0000000 --- a/data/minceraft/themes/custom/theme.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "colors": { - "AlternateBase": "#31363b", - "Base": "#232629", - "BrightText": "#ff0000", - "Button": "#31363b", - "ButtonText": "#ffffff", - "Highlight": "#2a82da", - "HighlightedText": "#000000", - "Link": "#2a82da", - "Text": "#ffffff", - "ToolTipBase": "#ffffff", - "ToolTipText": "#ffffff", - "Window": "#31363b", - "WindowText": "#ffffff", - "fadeAmount": 0.5, - "fadeColor": "#31363b" - }, - "name": "Custom", - "widgets": "Fusion" -} diff --git a/data/minceraft/themes/custom/themeStyle.css b/data/minceraft/themes/custom/themeStyle.css deleted file mode 100644 index 9fce8ae..0000000 --- a/data/minceraft/themes/custom/themeStyle.css +++ /dev/null @@ -1 +0,0 @@ -QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; } \ No newline at end of file diff --git a/data/minceraft/translations/index_v2.json b/data/minceraft/translations/index_v2.json deleted file mode 100755 index dd69a47..0000000 --- a/data/minceraft/translations/index_v2.json +++ /dev/null @@ -1,454 +0,0 @@ -{ - "file_type" : "MMC-TRANSLATION-INDEX", - "version" : 2, - "languages" : { - "ar" : { - "file" : "eb6a15e60a23901ea6a8005d8e9125bed4922f3d.class", - "sha1" : "eb6a15e60a23901ea6a8005d8e9125bed4922f3d", - "size" : 126775, - "translated" : 988, - "fuzzy" : 0, - "untranslated" : 105 - }, - "be" : { - "file" : "339e0b746e7908755f426b0d1c350acb072079d8.class", - "sha1" : "339e0b746e7908755f426b0d1c350acb072079d8", - "size" : 140090, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "bg" : { - "file" : "b6395a0c931c2e2a880224cee74d01fa969467c1.class", - "sha1" : "b6395a0c931c2e2a880224cee74d01fa969467c1", - "size" : 112989, - "translated" : 816, - "fuzzy" : 0, - "untranslated" : 277 - }, - "ca" : { - "file" : "57cdcee61737e6c0f7218899558e67fecea9e262.class", - "sha1" : "57cdcee61737e6c0f7218899558e67fecea9e262", - "size" : 141064, - "translated" : 975, - "fuzzy" : 0, - "untranslated" : 118 - }, - "cs" : { - "file" : "8b2ecbc4e2a439f27bdcaab5b2189c02cbcd079c.class", - "sha1" : "8b2ecbc4e2a439f27bdcaab5b2189c02cbcd079c", - "size" : 138537, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "cy" : { - "file" : "5c65378e6b7933dbf45ed7dd2a0deb0836d45de9.class", - "sha1" : "5c65378e6b7933dbf45ed7dd2a0deb0836d45de9", - "size" : 76254, - "translated" : 551, - "fuzzy" : 0, - "untranslated" : 542 - }, - "da" : { - "file" : "6fd5896ee6ed59f6803a446a8445b2f4e236268b.class", - "sha1" : "6fd5896ee6ed59f6803a446a8445b2f4e236268b", - "size" : 66801, - "translated" : 550, - "fuzzy" : 0, - "untranslated" : 543 - }, - "de" : { - "file" : "144b5c6ceb391a6b2e196356344d7e9abca97b88.class", - "sha1" : "144b5c6ceb391a6b2e196356344d7e9abca97b88", - "size" : 147827, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "de_CH" : { - "file" : "df553b69076153b1b9e0dcd045ebaf614a1703bc.class", - "sha1" : "df553b69076153b1b9e0dcd045ebaf614a1703bc", - "size" : 147899, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "el" : { - "file" : "48fdfc6fef33430de278a80a6a47db38e21bb78f.class", - "sha1" : "48fdfc6fef33430de278a80a6a47db38e21bb78f", - "size" : 142770, - "translated" : 986, - "fuzzy" : 0, - "untranslated" : 107 - }, - "en_GB" : { - "file" : "b1e7c270b460947711de28fbc7e6eabf292db4e0.class", - "sha1" : "b1e7c270b460947711de28fbc7e6eabf292db4e0", - "size" : 136327, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "eo" : { - "file" : "b3386dfd7d2c263598ebaf1950eb94e1cf20a5ab.class", - "sha1" : "b3386dfd7d2c263598ebaf1950eb94e1cf20a5ab", - "size" : 24371, - "translated" : 218, - "fuzzy" : 0, - "untranslated" : 875 - }, - "es" : { - "file" : "c98f678f9356e19ef6b8017b40a3c92de9643f07.class", - "sha1" : "c98f678f9356e19ef6b8017b40a3c92de9643f07", - "size" : 155344, - "translated" : 1074, - "fuzzy" : 0, - "untranslated" : 19 - }, - "es_UY" : { - "file" : "2e7156bb88c2bec8a0ed2c010aba81b822f1cd6d.class", - "sha1" : "2e7156bb88c2bec8a0ed2c010aba81b822f1cd6d", - "size" : 148293, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "et" : { - "file" : "e14f350b7de44997805853f859a0451a16ac483f.class", - "sha1" : "e14f350b7de44997805853f859a0451a16ac483f", - "size" : 110023, - "translated" : 881, - "fuzzy" : 0, - "untranslated" : 212 - }, - "fa" : { - "file" : "756c328866b85302445ec1ae55e11b4c10831fda.class", - "sha1" : "756c328866b85302445ec1ae55e11b4c10831fda", - "size" : 135579, - "translated" : 1020, - "fuzzy" : 0, - "untranslated" : 73 - }, - "fi" : { - "file" : "bdc7421f620fced85d84fd05ce7a2ef9eb89c3e1.class", - "sha1" : "bdc7421f620fced85d84fd05ce7a2ef9eb89c3e1", - "size" : 141197, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "fr" : { - "file" : "c3172e77e068e615c3bf4a447361f4b7ef0da764.class", - "sha1" : "c3172e77e068e615c3bf4a447361f4b7ef0da764", - "size" : 164857, - "translated" : 1093, - "fuzzy" : 0, - "untranslated" : 0 - }, - "gl" : { - "file" : "495c4cc380a80fa25163a2a2622f2643414831f0.class", - "sha1" : "495c4cc380a80fa25163a2a2622f2643414831f0", - "size" : 156017, - "translated" : 1093, - "fuzzy" : 0, - "untranslated" : 0 - }, - "he" : { - "file" : "5c4fac508c9c85f001980d11c6a6b64d26c4c510.class", - "sha1" : "5c4fac508c9c85f001980d11c6a6b64d26c4c510", - "size" : 127071, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "hu" : { - "file" : "37bd219c4a50c12ba70a0108f5fb5153731ee3a0.class", - "sha1" : "37bd219c4a50c12ba70a0108f5fb5153731ee3a0", - "size" : 144644, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "hy" : { - "file" : "2b6b042c110594ec1e8af5c77ba9e91d34030c3e.class", - "sha1" : "2b6b042c110594ec1e8af5c77ba9e91d34030c3e", - "size" : 6603, - "translated" : 102, - "fuzzy" : 0, - "untranslated" : 991 - }, - "id" : { - "file" : "a6e0bf75acff59c31c49265b5414170f50e0e2e5.class", - "sha1" : "a6e0bf75acff59c31c49265b5414170f50e0e2e5", - "size" : 135268, - "translated" : 984, - "fuzzy" : 0, - "untranslated" : 109 - }, - "is" : { - "file" : "061661de40508f2b5c044bbf7df314a2897768a4.class", - "sha1" : "061661de40508f2b5c044bbf7df314a2897768a4", - "size" : 11020, - "translated" : 145, - "fuzzy" : 0, - "untranslated" : 948 - }, - "it" : { - "file" : "f91c8470d31c6485417df6840c2638a21a80f8b1.class", - "sha1" : "f91c8470d31c6485417df6840c2638a21a80f8b1", - "size" : 153828, - "translated" : 1061, - "fuzzy" : 0, - "untranslated" : 32 - }, - "ja" : { - "file" : "c8cb1d373557a58e5e85c5fc628f40233660dcf3.class", - "sha1" : "c8cb1d373557a58e5e85c5fc628f40233660dcf3", - "size" : 110002, - "translated" : 1016, - "fuzzy" : 0, - "untranslated" : 77 - }, - "ja_KANJI" : { - "file" : "509c74b64179f0bac0d07c794b3f2ac73d8c8b00.class", - "sha1" : "509c74b64179f0bac0d07c794b3f2ac73d8c8b00", - "size" : 107852, - "translated" : 997, - "fuzzy" : 0, - "untranslated" : 96 - }, - "jv" : { - "file" : "4884fd9af6890647b7af1aefa57f38cca49ad899.class", - "sha1" : "4884fd9af6890647b7af1aefa57f38cca49ad899", - "size" : 16, - "translated" : 0, - "fuzzy" : 0, - "untranslated" : 1030 - }, - "ka" : { - "file" : "f50883cad9dd7abbadba01a4303cfd1014ae2b21.class", - "sha1" : "f50883cad9dd7abbadba01a4303cfd1014ae2b21", - "size" : 34461, - "translated" : 253, - "fuzzy" : 0, - "untranslated" : 840 - }, - "kn" : { - "file" : "2aa2d42c51f9cf024e3777f0dde4270388fd22ae.class", - "sha1" : "2aa2d42c51f9cf024e3777f0dde4270388fd22ae", - "size" : 23, - "translated" : 0, - "fuzzy" : 0, - "untranslated" : 1030 - }, - "ko" : { - "file" : "5e5506a3c2ea90e1c396b54f070a2620f20fbe30.class", - "sha1" : "5e5506a3c2ea90e1c396b54f070a2620f20fbe30", - "size" : 112422, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "lb" : { - "file" : "e588e4755a1e407314bf3753572744514e04bea6.class", - "sha1" : "e588e4755a1e407314bf3753572744514e04bea6", - "size" : 21524, - "translated" : 152, - "fuzzy" : 0, - "untranslated" : 941 - }, - "lt" : { - "file" : "969a032f2aca58c6cd8def04aed39763e923077b.class", - "sha1" : "969a032f2aca58c6cd8def04aed39763e923077b", - "size" : 81459, - "translated" : 701, - "fuzzy" : 0, - "untranslated" : 392 - }, - "lv" : { - "file" : "4453f6a649d5a079b116aa742bac2207a796860c.class", - "sha1" : "4453f6a649d5a079b116aa742bac2207a796860c", - "size" : 139731, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "mk" : { - "file" : "5bd1feff346ff9e945079856554d174075d5f5c7.class", - "sha1" : "5bd1feff346ff9e945079856554d174075d5f5c7", - "size" : 13284, - "translated" : 86, - "fuzzy" : 0, - "untranslated" : 1007 - }, - "mn" : { - "file" : "ba4455b0bfcc24d23499587637aebc9ce283000e.class", - "sha1" : "ba4455b0bfcc24d23499587637aebc9ce283000e", - "size" : 145103, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "ms" : { - "file" : "0a2440c5ba866d5813b33fbaae04568dea18dfe8.class", - "sha1" : "0a2440c5ba866d5813b33fbaae04568dea18dfe8", - "size" : 128928, - "translated" : 958, - "fuzzy" : 0, - "untranslated" : 135 - }, - "nb" : { - "file" : "c5c8b5642165dcb28604bd40d7f2e6a508e51725.class", - "sha1" : "c5c8b5642165dcb28604bd40d7f2e6a508e51725", - "size" : 65006, - "translated" : 607, - "fuzzy" : 0, - "untranslated" : 486 - }, - "nl" : { - "file" : "3fced84194ef345022dddee84738bb8c742012c5.class", - "sha1" : "3fced84194ef345022dddee84738bb8c742012c5", - "size" : 145861, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "nn" : { - "file" : "9b568b72e01e6e3a805be3fcac81a8d2a81487e8.class", - "sha1" : "9b568b72e01e6e3a805be3fcac81a8d2a81487e8", - "size" : 137945, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "pl" : { - "file" : "a3060323d787011872c3d8aae9f2f6660a40cf9c.class", - "sha1" : "a3060323d787011872c3d8aae9f2f6660a40cf9c", - "size" : 143293, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "pt_PT" : { - "file" : "cba7ae8c6b93047045ab92a42651d5e9bdd654da.class", - "sha1" : "cba7ae8c6b93047045ab92a42651d5e9bdd654da", - "size" : 144795, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "pt_BR" : { - "file" : "f7c9ce421a50ce1f07e33c521bf63021eaabf39c.class", - "sha1" : "f7c9ce421a50ce1f07e33c521bf63021eaabf39c", - "size" : 141123, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "ro" : { - "file" : "937ae3c194c0857ed9b804a229efce92fa4c5674.class", - "sha1" : "937ae3c194c0857ed9b804a229efce92fa4c5674", - "size" : 144892, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "ru" : { - "file" : "6ef2160cd14f59f23bda3df995da9bdc7e28cbba.class", - "sha1" : "6ef2160cd14f59f23bda3df995da9bdc7e28cbba", - "size" : 142835, - "translated" : 1031, - "fuzzy" : 0, - "untranslated" : 62 - }, - "si" : { - "file" : "b156f906a02decf93cf1f988984c7b5a43409590.class", - "sha1" : "b156f906a02decf93cf1f988984c7b5a43409590", - "size" : 454, - "translated" : 7, - "fuzzy" : 0, - "untranslated" : 1086 - }, - "sk" : { - "file" : "f1d32f055feac63a4c9e773c3e66ab7b4ae35626.class", - "sha1" : "f1d32f055feac63a4c9e773c3e66ab7b4ae35626", - "size" : 139643, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "sl" : { - "file" : "8c66381fe0bf8e858a41bac30d28240c3c7d2d9a.class", - "sha1" : "8c66381fe0bf8e858a41bac30d28240c3c7d2d9a", - "size" : 83079, - "translated" : 599, - "fuzzy" : 0, - "untranslated" : 494 - }, - "sr" : { - "file" : "e28850462023c55c1ec87117f342039ff9251e6a.class", - "sha1" : "e28850462023c55c1ec87117f342039ff9251e6a", - "size" : 55187, - "translated" : 450, - "fuzzy" : 0, - "untranslated" : 643 - }, - "sv" : { - "file" : "99ab207f3d99a59b8173a2ee5eedb8cdb79e9b1a.class", - "sha1" : "99ab207f3d99a59b8173a2ee5eedb8cdb79e9b1a", - "size" : 134617, - "translated" : 990, - "fuzzy" : 0, - "untranslated" : 103 - }, - "th" : { - "file" : "805e8d895166a8942afc90fed6b693dee1a8b134.class", - "sha1" : "805e8d895166a8942afc90fed6b693dee1a8b134", - "size" : 48926, - "translated" : 405, - "fuzzy" : 0, - "untranslated" : 688 - }, - "tr" : { - "file" : "7454355d341c712932b707d70faec9d1f7ca3e7b.class", - "sha1" : "7454355d341c712932b707d70faec9d1f7ca3e7b", - "size" : 139854, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "uk" : { - "file" : "1ff78912f28968ee7149818c5fcb20d9ad123aee.class", - "sha1" : "1ff78912f28968ee7149818c5fcb20d9ad123aee", - "size" : 143599, - "translated" : 1038, - "fuzzy" : 0, - "untranslated" : 55 - }, - "vi" : { - "file" : "95cced97bbf4706976d133113aaebce1d8d6dd0e.class", - "sha1" : "95cced97bbf4706976d133113aaebce1d8d6dd0e", - "size" : 139812, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "zh" : { - "file" : "1764ce984b322bf8ab2f88f93c47db62261df4a3.class", - "sha1" : "1764ce984b322bf8ab2f88f93c47db62261df4a3", - "size" : 101420, - "translated" : 1024, - "fuzzy" : 0, - "untranslated" : 69 - }, - "zh_TW" : { - "file" : "53db715fc238e97bee26c4b2e537076324063979.class", - "sha1" : "53db715fc238e97bee26c4b2e537076324063979", - "size" : 108891, - "translated" : 1092, - "fuzzy" : 0, - "untranslated" : 1 - } - } -} diff --git a/data/minceraft/translations/mmc_ru.qm b/data/minceraft/translations/mmc_ru.qm deleted file mode 100755 index 140e845..0000000 Binary files a/data/minceraft/translations/mmc_ru.qm and /dev/null differ diff --git a/data/minceraft/ultimmc.cfg b/data/minceraft/ultimmc.cfg deleted file mode 100755 index 0cfee9a..0000000 --- a/data/minceraft/ultimmc.cfg +++ /dev/null @@ -1,51 +0,0 @@ -Analytics=true -AutoCloseConsole=false -AutoUpdate=true -CentralModsDir=mods -ConsoleFont=DejaVu Sans Mono -ConsoleFontSize=11 -ConsoleMaxLines=100000 -ConsoleOverflowStop=true -ConsoleWindowGeometry=AdnQywADAAAAAAUCAAAAMAAAB30AAAStAAAAAAAAAAD////+/////gAAAAACAAAAB4AAAAUCAAAAMAAAB30AAASt -ConsoleWindowState=AAAA/wAAAAD9AAAAAAAAAnwAAAR+AAAABAAAAAQAAAAIAAAACPwAAAAA -IconTheme=multimc -IconsDir=icons -InstSortMode=Name -InstanceDir=instances -JProfilerPath= -JVisualVMPath= -JavaPath=java -JsonEditor= -JvmArgs= -Language=ru -LastHostname=minceraftos -LastUsedGroupForNewInstance= -LaunchMaximized=false -MCEditPath= -MainWindowGeometry=AdnQywADAAAAAAKCAAAAMAAABP0AAAStAAAAAAAAABQAAAPVAAACYwAAAAACAAAAB4AAAAKCAAAAMAAABP0AAASt -MainWindowState=AAAA/wAAAAD9AAAAAAAAAZoAAAQiAAAABAAAAAQAAAAIAAAACPwAAAADAAAAAQAAAAEAAAAeAGkAbgBzAHQAYQBuAGMAZQBUAG8AbwBsAEIAYQByAwAAAAD/////AAAAAAAAAAAAAAACAAAAAQAAABYAbQBhAGkAbgBUAG8AbwBsAEIAYQByAQAAAAD/////AAAAAAAAAAAAAAADAAAAAQAAABYAbgBlAHcAcwBUAG8AbwBsAEIAYQByAQAAAAD/////AAAAAAAAAAA= -MaxMemAlloc=1024 -MinMemAlloc=512 -MinecraftWinHeight=480 -MinecraftWinWidth=854 -NewInstanceGeometry=AdnQywADAAAAAAJTAAABhQAABSwAAANBAAACUwAAAYUAAAUsAAADQQAAAAAAAAAAB4AAAAJTAAABhQAABSwAAANB -PagedGeometry=AdnQywADAAAAAAIuAAAA5QAABVAAAAPhAAACLgAAAOUAAAVQAAAD4QAAAAAAAAAAB4AAAAIuAAAA5QAABVAAAAPh -PasteEEAPIKey=multimc -PostExitCommand= -PreLaunchCommand= -ProxyAddr=127.0.0.1 -ProxyPass= -ProxyPort=8080 -ProxyType=None -ProxyUser= -RecordGameTime=true -SelectedInstance=1.21.4 -ShowConsole=false -ShowConsoleOnError=true -ShowGameTime=true -ShowGameTimeHours=false -ShowGlobalGameTime=true -ShownNotifications= -UseNativeGLFW=false -UseNativeOpenAL=false -WrapperCommand= diff --git a/data/minceraft/ultimmc.cfg.def b/data/minceraft/ultimmc.cfg.def old mode 100644 new mode 100755 diff --git a/ultimmc/.gitattributes b/ultimmc/.gitattributes new file mode 100644 index 0000000..c9c0d50 --- /dev/null +++ b/ultimmc/.gitattributes @@ -0,0 +1,2 @@ +*.pem -crlf +**/testdata/** -text -diff diff --git a/ultimmc/.github/ISSUE_TEMPLATE/bug_report.yml b/ultimmc/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..72986e9 --- /dev/null +++ b/ultimmc/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,51 @@ +name: Bug Report +description: File a bug report +labels: [bug, needs-triage] +body: +- type: markdown + attributes: + value: | + If you need help with running Minecraft, please visit us [on our Discord](https://discord.gg/multimc) before making a bug report. + + Before submitting a bug report, please make sure you have read this *entire* form, and that: + * You have read the [FAQ](https://github.com/MultiMC/Launcher/wiki/FAQ) and it has not answered your question + * Your bug is not caused by Minecraft or any mods you have installed. + * Your issue has not been reported before, [make sure to use the search function!](https://github.com/MultiMC/Launcher/issues) + + **Do not forget to give your issue a descriptive title.** "Bug in the instance screen" makes it hard to distinguish issues at a glance. +- type: dropdown + attributes: + label: Operating System + description: If you know this bug occurs on multiple operating systems, select all you have tested. + multiple: true + options: + - Windows + - macOS + - Linux + - Other +- type: textarea + attributes: + label: Description of bug + description: What did you expect to happen, what happened, and why is it incorrect? + placeholder: The cat button should show a cat, but it showed a dog instead! + validations: + required: true +- type: textarea + attributes: + label: Steps to reproduce + description: A bulleted list, or an exported instance if relevant. + placeholder: "* Press the cat button" + validations: + required: true +- type: textarea + attributes: + label: Suspected cause + description: If you know what could be causing this bug, describe it here. + validations: + required: false +- type: checkboxes + attributes: + label: This issue is unique + options: + - label: I have searched the issue tracker and did not find an issue describing my bug. + required: true diff --git a/ultimmc/.github/ISSUE_TEMPLATE/config.yml b/ultimmc/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0086358 --- /dev/null +++ b/ultimmc/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/ultimmc/.github/ISSUE_TEMPLATE/suggestion.yml b/ultimmc/.github/ISSUE_TEMPLATE/suggestion.yml new file mode 100644 index 0000000..88bf66c --- /dev/null +++ b/ultimmc/.github/ISSUE_TEMPLATE/suggestion.yml @@ -0,0 +1,38 @@ +name: Suggestion +description: Make a suggestion +labels: [idea, needs-triage] +body: +- type: markdown + attributes: + value: | + ### Use this form to suggest a feature for MultiMC. +- type: input + attributes: + label: Role + description: In what way do you use MultiMC that needs this feature? + placeholder: I play modded Minecraft. + validations: + required: true +- type: input + attributes: + label: Suggestion + description: What do you want MultiMC to do? + placeholder: I want the cat button to meow. + validations: + required: true +- type: input + attributes: + label: Benefit + description: Why do you need MultiMC to do this? + placeholder: so that I can always hear a cat when I need to. + validations: + required: true +- type: checkboxes + attributes: + label: This suggestion is unique + options: + - label: I have searched the issue tracker and did not find an issue describing my suggestion, especially not one that has been rejected. + required: true +- type: textarea + attributes: + label: You may use the editor below to elaborate further. diff --git a/ultimmc/.github/workflows/dispatch.yml b/ultimmc/.github/workflows/dispatch.yml new file mode 100644 index 0000000..097524c --- /dev/null +++ b/ultimmc/.github/workflows/dispatch.yml @@ -0,0 +1,20 @@ +name: Dispatcher +on: + push: + branches: ['6'] +jobs: + dispatch: + name: Dispatch + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Extract branch name + shell: bash + run: echo "branch=$(echo ${GITHUB_REF#refs/heads/})" >>$GITHUB_OUTPUT + id: extract_branch + - name: Dispatch to workflows + run: | + curl -H "Accept: application/vnd.github.everest-preview+json" \ + -H "Authorization: token ${{ secrets.DISPATCH_TOKEN }}" \ + --request POST \ + --data '{"event_type": "push_to_main_repo", "client_payload": { "branch": "${{ steps.extract_branch.outputs.branch }}" }}' https://api.github.com/repos/MultiMC/Build/dispatches diff --git a/ultimmc/.github/workflows/main.yml b/ultimmc/.github/workflows/main.yml new file mode 100644 index 0000000..6f45374 --- /dev/null +++ b/ultimmc/.github/workflows/main.yml @@ -0,0 +1,286 @@ +name: CI + +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + workflow_dispatch: + schedule: + - cron: "0 0 1 * *" + +jobs: + build-linux: + name: build-linux + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@main + with: + submodules: 'recursive' + + - name: Install Dependencies + run: | + sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32 + sudo add-apt-repository 'deb http://dk.archive.ubuntu.com/ubuntu/ bionic main' + sudo add-apt-repository 'deb http://dk.archive.ubuntu.com/ubuntu/ bionic universe' + sudo apt update + sudo apt install libgl1-mesa-dev qttools5-dev g++-5 gcc-5 + + - name: Build + run: | + export JAVA_HOME=$JAVA_HOME_8_X64 + mkdir build + cd build + cmake \ + -DCMAKE_C_COMPILER=/usr/bin/gcc-5 \ + -DCMAKE_CXX_COMPILER=/usr/bin/g++-5 \ + -DCMAKE_BUILD_TYPE=Release \ + -DLauncher_NOTIFICATION_URL:STRING=https://files.multimc.org/notifications.json \ + -DCMAKE_INSTALL_PREFIX:PATH=/home/runner/UltimMC/UltimMC \ + -DLauncher_UPDATER_BASE=https://files.multimc.org/update/ \ + -DLauncher_PASTE_EE_API_KEY:STRING=utLvciUouSURFzfjPxLBf5W4ISsUX4pwBDF7N1AfZ \ + -DLauncher_ANALYTICS_ID:STRING=UA-87731965-2 \ + -DLauncher_LAYOUT=lin-nodeps \ + -DLauncher_BUILD_PLATFORM=lin64 \ + -DLauncher_BUG_TRACKER_URL=https://github.com/UltimMC/Launcher/issues \ + -DLauncher_EMBED_SECRETS=On \ + $GITHUB_WORKSPACE + + - name: Compile + run: | + cd build + make -j$(nproc) + + - name: Test + run: | + cd build + make test + cmake -E remove_directory "/home/runner/UltimMC/UltimMC" + + - name: Install + run: | + cd build + make install + chmod +x /home/runner/UltimMC/UltimMC/UltimMC + chmod +x /home/runner/UltimMC/UltimMC/bin/UltimMC + + - name: Upload Artifacts + uses: actions/upload-artifact@main + with: + name: mmc-cracked-lin64 + path: /home/runner/UltimMC + + build-windows: + name: build-windows + runs-on: windows-latest + + steps: + - uses: actions/checkout@main + with: + submodules: 'recursive' + + - name: Cache Qt + uses: actions/cache@main + id: qt-cached + with: + path: "D:/Qt" + key: ${{ runner.os }}-qt56-installed-d + + - name: Cache Qt Installer + uses: actions/cache@main + if: steps.qt-cached.outputs.cache-hit != 'true' + id: installer-cached + with: + path: "installer.exe" + key: ${{ runner.os }}-qt56-installer + + - name: Create QtAccount File + if: steps.qt-cached.outputs.cache-hit != 'true' + run: | + mkdir C:/Users/runneradmin/AppData/Roaming/Qt/ + curl https://gist.github.com/Neptune650/1086e0a3126be6a66580b71afcf8bd99/raw/797d8b90edf07ce88f265b38a573cc6b1fb45bfb/qtaccount.txt --output C:/Users/runneradmin/AppData/Roaming/Qt/qtaccount.ini + + - name: Download Qt Installer + if: steps.installer-cached.outputs.cache-hit != 'true' && steps.qt-cached.outputs.cache-hit != 'true' + run: curl https://download.qt.io/new_archive/qt/5.6/5.6.3/qt-opensource-windows-x86-mingw492-5.6.3.exe --output installer.exe + + - name: Download Qt non-Interactive Script + if: steps.qt-cached.outputs.cache-hit != 'true' + run: curl https://gist.githubusercontent.com/Neptune650/aa6c051abc17e7d9d609add7f6dfd16a/raw/074dedb7525c0ffc010b39871615b008c2efbcd6/qt-installer-noninteractive.qs --output nonInteractive.qs + + - name: Install Qt + if: steps.qt-cached.outputs.cache-hit != 'true' + shell: cmd + run: installer.exe -v --script nonInteractive.qs --silent + + - name: Setup CMake + run: | + curl -L https://github.com/Kitware/CMake/releases/download/v3.30.4/cmake-3.30.4-windows-i386.zip -o cmake.zip + unzip cmake.zip + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '8' + architecture: x86 + + - name: Setup zlib + run: | + mkdir zlib + cd zlib + C:\msys64\usr\bin\wget.exe -O zlib.zip https://downloads.sourceforge.net/project/gnuwin32/zlib/1.2.3/zlib-1.2.3-bin.zip + C:\msys64\usr\bin\wget.exe -O zliblibs.zip https://downloads.sourceforge.net/project/gnuwin32/zlib/1.2.3/zlib-1.2.3-lib.zip + unzip zlib.zip + unzip zliblibs.zip + + - name: Setup OpenSSL + run: | + mkdir OpenSSL + cd OpenSSL + curl -L https://files.catbox.moe/ctwswu.dll -o libeay32.dll + curl -L https://files.catbox.moe/ie9e77.dll -o ssleay32.dll + + - name: Build + shell: cmd + if: steps.build-cached.outputs.cache-hit != 'true' + run: | + for /f "tokens=*" %%n in ('powershell -NoLogo -Command "$(ls $pwd\cmake-*-windows-i386\bin).Fullname"') do @(set PATHCM=%%n) + set PATH=D:\Qt\5.6.3\mingw49_32\bin;D:\Qt\Tools\mingw492_32\bin; + set PATH=%CD%\zlib;%CD%\zlib\bin;%CD%\zlib\lib;%CD%\zlib\include;%PATH% + set PATH=%CD%\OpenSSL;%PATH% + set PATH=%PATHCM%;%PATH% + mkdir build + cd build + cmake ^ + -DCMAKE_C_COMPILER=gcc ^ + -DCMAKE_CXX_COMPILER=g++ ^ + -DCMAKE_BUILD_TYPE=Release ^ + -DLauncher_NOTIFICATION_URL:STRING=https://files.multimc.org/notifications.json ^ + -DCMAKE_INSTALL_PREFIX:PATH="D:/UltimMC/UltimMC" ^ + -DCMAKE_PREFIX_PATH="D:\Qt\5.6.3\mingw49_32" ^ + -DQt5_DIR="D:\Qt\5.6.3\mingw49_32" ^ + -DLauncher_UPDATER_BASE=https://files.multimc.org/update/ ^ + -DLauncher_PASTE_EE_API_KEY:STRING=utLvciUouSURFzfjPxLBf5W4ISsUX4pwBDF7N1AfZ ^ + -DLauncher_ANALYTICS_ID:STRING=UA-87731965-2 ^ + -DLauncher_LAYOUT=win-bundle ^ + -DLauncher_BUILD_PLATFORM=win32 ^ + -DLauncher_BUG_TRACKER_URL=https://github.com/UltimMC/Launcher/issues ^ + -DLauncher_EMBED_SECRETS=On ^ + -G "MinGW Makefiles" ^ + .. + + - name: Compile + shell: cmd + run: | + for /f "tokens=*" %%n in ('powershell -NoLogo -Command "$(ls $pwd\cmake-*-windows-i386\bin).Fullname"') do @(set PATHCM=%%n) + set PATH=D:\Qt\5.6.3\mingw49_32\bin;D:\Qt\Tools\mingw492_32\bin; + set PATH=%CD%\zlib;%CD%\zlib\bin;%PATH% + set PATH=%CD%\OpenSSL;%PATH% + set PATH=%PATHCM%;%PATH% + set PATH=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;%PATH% + cd build + mingw32-make -j%NUMBER_OF_PROCESSORS% + + - name: Test + shell: cmd + run: | + for /f "tokens=*" %%n in ('powershell -NoLogo -Command "$(ls $pwd\cmake-*-windows-i386\bin).Fullname"') do @(set PATHCM=%%n) + set PATH=D:\Qt\5.6.3\mingw49_32\bin;D:\Qt\Tools\mingw492_32\bin; + set PATH=%CD%\zlib;%CD%\zlib\bin;%PATH% + set PATH=%CD%\OpenSSL;%PATH% + set PATH=%PATHCM%;%PATH% + set PATH=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;%PATH% + cd build + mingw32-make test + cmake -E remove_directory "D:/UltimMC/UltimMC" + + - name: Install + shell: cmd + run: | + for /f "tokens=*" %%n in ('powershell -NoLogo -Command "$(ls $pwd\cmake-*-windows-i386\bin).Fullname"') do @(set PATHCM=%%n) + set PATH=D:\Qt\5.6.3\mingw49_32\bin;D:\Qt\Tools\mingw492_32\bin; + set PATH=%CD%\zlib;%CD%\zlib\bin;%PATH% + set PATH=%CD%\OpenSSL;%PATH% + set PATH=%PATHCM%;%PATH% + set PATH=C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;%PATH% + cd build + mingw32-make install + + - name: Copy OpenSSL + shell: cmd + run: | + cp OpenSSL/ssleay32.dll D:/UltimMC/UltimMC/ssleay32.dll + cp OpenSSL/libeay32.dll D:/UltimMC/UltimMC/libeay32.dll + + - name: Upload Artifacts + uses: actions/upload-artifact@main + with: + name: mmc-cracked-win32 + path: "D:/UltimMC" + + build-mac: + name: build-mac + runs-on: macos-13 + + steps: + - uses: actions/checkout@main + with: + submodules: 'recursive' + + - name: Cache Dependencies + uses: actions/cache@main + with: + path: /Users/runner/Library/Caches/Homebrew + key: ${{ runner.os }}-deps-cache + + - name: Install Dependencies + run: | + brew cleanup + brew install qt@5 + + - name: Build + run: | + mkdir build + cd build + cmake \ + -DCMAKE_C_COMPILER=/usr/bin/clang \ + -DCMAKE_CXX_COMPILER=/usr/bin/clang++ \ + -DCMAKE_BUILD_TYPE=Release \ + -DLauncher_NOTIFICATION_URL:STRING=https://files.multimc.org/notifications.json \ + -DCMAKE_INSTALL_PREFIX:PATH="/Users/runner/work/UltimMC/build/dist" \ + -DCMAKE_PREFIX_PATH="$(brew --prefix qt@5)/lib/cmake" \ + -DQt5_DIR="$(brew --prefix qt@5)" \ + -DLauncher_UPDATER_BASE=https://files.multimc.org/update/ \ + -DLauncher_PASTE_EE_API_KEY:STRING=utLvciUouSURFzfjPxLBf5W4ISsUX4pwBDF7N1AfZ \ + -DLauncher_ANALYTICS_ID:STRING=UA-87731965-2 \ + -DLauncher_LAYOUT=mac-bundle \ + -DLauncher_BUILD_PLATFORM=osx64-5.15.2 \ + -DLauncher_BUG_TRACKER_URL=https://github.com/UltimMC/Launcher/issues \ + -DCMAKE_OSX_DEPLOYMENT_TARGET=10.13 \ + -DLauncher_EMBED_SECRETS=On \ + $GITHUB_WORKSPACE + + - name: Compile + run: | + cd build + make -j$(sysctl -n hw.logicalcpu) + + - name: Test + run: | + cd build + make test + cmake -E remove_directory "/Users/runner/work/UltimMC/build/dist" + + - name: Install + run: | + cd build + make install + chmod +x /Users/runner/work/UltimMC/build/dist/UltimMC.app/Contents/MacOS/UltimMC + + - name: Upload Artifacts + uses: actions/upload-artifact@main + with: + name: mmc-cracked-osx64 + path: /Users/runner/work/UltimMC/build/dist diff --git a/ultimmc/.gitignore b/ultimmc/.gitignore new file mode 100644 index 0000000..f1bd42a --- /dev/null +++ b/ultimmc/.gitignore @@ -0,0 +1,39 @@ +Thumbs.db +*.kdev4 +.user +.directory +resources/CMakeFiles +*~ +*.swp +html/ + +# Project Files +*.pro.user +CMakeLists.txt.user +CMakeLists.txt.user.* +/.project +/.settings +/.idea +cmake-build-*/ +Debug +.cache + +# Build dirs +build +/build-* + +# Install dirs +install +/install-* + +# Ctags File +tags + +# YouCompleteMe config stuff. +.ycm_extra_conf.* + +#OSX Stuff +.DS_Store + +branding/ +run/ diff --git a/ultimmc/.gitmodules b/ultimmc/.gitmodules new file mode 100644 index 0000000..04a561c --- /dev/null +++ b/ultimmc/.gitmodules @@ -0,0 +1,8 @@ +[submodule "depends/libnbtplusplus"] + path = libraries/libnbtplusplus + url = https://github.com/MultiMC/libnbtplusplus.git + pushurl = git@github.com:MultiMC/libnbtplusplus.git +[submodule "libraries/quazip"] + path = libraries/quazip + url = https://github.com/MultiMC/quazip.git + pushurl = git@github.com:MultiMC/quazip.git diff --git a/ultimmc/CMakeLists.txt b/ultimmc/CMakeLists.txt new file mode 100644 index 0000000..e9ac52b --- /dev/null +++ b/ultimmc/CMakeLists.txt @@ -0,0 +1,290 @@ +cmake_minimum_required(VERSION 3.1) + +if(WIN32) + # In Qt 5.1+ we have our own main() function, don't autolink to qtmain on Windows + cmake_policy(SET CMP0020 OLD) +endif() + +project(Launcher) +enable_testing() + +string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD) +if(IS_IN_SOURCE_BUILD) + message(FATAL_ERROR "You are building the Launcher in-source. Please separate the build tree from the source tree.") +endif() + +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + if(CMAKE_HOST_SYSTEM_VERSION MATCHES ".*[Mm]icrosoft.*" OR + CMAKE_HOST_SYSTEM_VERSION MATCHES ".*WSL.*" + ) + message(FATAL_ERROR "Building the Launcher is not supported in Linux-on-Windows distributions.") + endif() +endif() + +option(Launcher_RUN_CLANG_TIDY "Run clang-tidy with the compiler." OFF) +if(Launcher_RUN_CLANG_TIDY) + find_program(CLANG_TIDY_COMMAND NAMES clang-tidy) + if(NOT CLANG_TIDY_COMMAND) + message(WARNING "CMake_RUN_CLANG_TIDY is ON but clang-tidy is not found!") + set(DO_CLANG_TIDY "" CACHE STRING "" FORCE) + else() + set(CLANG_TIDY_CHECKS "-modernize-use-trailing-return-type,-readability-magic-numbers,-modernize-avoid-c-arrays") + set(DO_CLANG_TIDY "${CLANG_TIDY_COMMAND};-checks=${CLANG_TIDY_CHECKS};-header-filter='${CMAKE_SOURCE_DIR}/src/*'") + endif() + # TODO: make this per-target, differentiate between libraries and main codebase + set(CMAKE_CXX_CLANG_TIDY ${DO_CLANG_TIDY}) +endif() + + +##################################### Set CMake options ##################################### +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/") + +# Output all executables and shared libs in the main build folder, not in subfolders. +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) +if(UNIX) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}) +endif() +set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${PROJECT_BINARY_DIR}/jars) + +######## Set compiler flags ######## +set(CMAKE_CXX_STANDARD_REQUIRED true) +set(CMAKE_C_STANDARD_REQUIRED true) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_C_STANDARD 11) +include(GenerateExportHeader) +set(CMAKE_CXX_FLAGS " -Wall -pedantic -Wno-deprecated-declarations -D_GLIBCXX_USE_CXX11_ABI=0 -fstack-protector-strong --param=ssp-buffer-size=4 ${CMAKE_CXX_FLAGS}") +set(CMAKE_CXX_FLAGS_RELEASE " -O3 -D_FORTIFY_SOURCE=2 ${CMAKE_CXX_FLAGS_RELEASE}") +if(UNIX AND APPLE) + set(CMAKE_CXX_FLAGS " -stdlib=libc++ ${CMAKE_CXX_FLAGS}") +endif() +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Werror -Werror=return-type -O0") + +# Fix build with Qt 5.13 +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DQT_NO_DEPRECATED_WARNINGS=Y") + +##################################### Set Application options ##################################### + +######## Set URLs ######## +set(Launcher_NEWS_RSS_URL "https://multimc.org/rss.xml" CACHE STRING "URL to fetch Launcher's news RSS feed from.") + +######## Set version numbers ######## +set(Launcher_VERSION_MAJOR 0) +set(Launcher_VERSION_MINOR 7) +set(Launcher_VERSION_HOTFIX 0) + +# Build number +set(Launcher_VERSION_BUILD -1 CACHE STRING "Build number. -1 for no build number.") + +# Build platform. +set(Launcher_BUILD_PLATFORM "" CACHE STRING "A short string identifying the platform that this build was built for. Only used by the notification system and to display in the about dialog.") + +# Channel list URL +set(Launcher_UPDATER_BASE "" CACHE STRING "Base URL for the updater.") + +# Notification URL +set(Launcher_NOTIFICATION_URL "" CACHE STRING "URL for checking for notifications.") + +# The metadata server +set(Launcher_META_URL "https://meta.multimc.org/v1/" CACHE STRING "URL to fetch Launcher's meta files from.") + +# paste.ee API key +set(Launcher_PASTE_EE_API_KEY "utLvciUouSURFzfjPxLBf5W4ISsUX4pwBDF7N1AfZ" CACHE STRING "API key you can get from paste.ee when you register an account") + +# Imgur API Client ID +set(Launcher_IMGUR_CLIENT_ID "5b97b0713fba4a3" CACHE STRING "Client ID you can get from Imgur when you register an application") + +# Google analytics ID +set(Launcher_ANALYTICS_ID "UA-87731965-2" CACHE STRING "ID you can get from Google analytics") + +# Bug tracker URL +set(Launcher_BUG_TRACKER_URL "" CACHE STRING "URL for the bug tracker.") + +# Discord URL +set(Launcher_DISCORD_URL "" CACHE STRING "URL for the Discord guild.") + +# Subreddit URL +set(Launcher_SUBREDDIT_URL "" CACHE STRING "URL for the subreddit.") + +# Use the secrets library or a public stub? +option(Launcher_EMBED_SECRETS "Determines whether to embed secrets. Secrets are separate and non-public." OFF) + +# API Keys +# NOTE: These API keys are here for convenience. If you rebrand this software or intend to break the terms of service +# of these platforms, please change these API keys beforehand. +# Be aware that if you were to use these API keys for malicious purposes they might get revoked, which might cause +# breakage to thousands of users. +# If you don't plan to use these features of this software, you can just remove these values. + +# By using this key in your builds you accept the terms of use laid down in +# https://docs.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use +set(Launcher_MSA_CLIENT_ID "f4404707-7bbe-4e40-80ba-85fb2bb825a1" CACHE STRING "Client ID you can get from Microsoft Identity Platform when you register an application") + +#### Check the current Git commit and branch +include(GetGitRevisionDescription) +get_git_head_revision(Launcher_GIT_REFSPEC Launcher_GIT_COMMIT) + +message(STATUS "Git commit: ${Launcher_GIT_COMMIT}") +message(STATUS "Git refspec: ${Launcher_GIT_REFSPEC}") + +set(Launcher_RELEASE_VERSION_NAME "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}") + +#### Custom target to just print the version. +add_custom_target(version echo "Version: ${Launcher_RELEASE_VERSION_NAME}") +add_custom_target(tcversion echo "\\#\\#teamcity[setParameter name=\\'env.LAUNCHER_VERSION\\' value=\\'${Launcher_RELEASE_VERSION_NAME}\\']") + +################################ 3rd Party Libs ################################ + +# Find the required Qt parts +find_package(Qt5Core REQUIRED) +find_package(Qt5Widgets REQUIRED) +find_package(Qt5Concurrent REQUIRED) +find_package(Qt5Network REQUIRED) +find_package(Qt5Test REQUIRED) +find_package(Qt5Xml REQUIRED) + +# The Qt5 cmake files don't provide its install paths, so ask qmake. +include(QMakeQuery) +query_qmake(QT_INSTALL_PLUGINS QT_PLUGINS_DIR) +query_qmake(QT_INSTALL_IMPORTS QT_IMPORTS_DIR) +query_qmake(QT_INSTALL_LIBS QT_LIBS_DIR) +query_qmake(QT_INSTALL_LIBEXECS QT_LIBEXECS_DIR) +query_qmake(QT_HOST_DATA QT_DATA_DIR) +set(QT_MKSPECS_DIR ${QT_DATA_DIR}/mkspecs) + +if (Qt5_POSITION_INDEPENDENT_CODE) + SET(CMAKE_POSITION_INDEPENDENT_CODE ON) +endif() + +####################################### Secrets ####################################### + +if(Launcher_EMBED_SECRETS) + add_subdirectory(secrets) +else() + add_subdirectory(notsecrets) +endif() + +####################################### Install layout ####################################### + +# How to install the build results +set(Launcher_LAYOUT "auto" CACHE STRING "The layout for the launcher installation (auto, win-bundle, lin-nodeps, mac-bundle)") +set_property(CACHE Launcher_LAYOUT PROPERTY STRINGS auto win-bundle lin-nodeps mac-bundle) + +if(Launcher_LAYOUT STREQUAL "auto") + if(UNIX AND APPLE) + set(Launcher_LAYOUT_REAL "mac-bundle") + elseif(UNIX) + set(Launcher_LAYOUT_REAL "lin-nodeps") + elseif(WIN32) + set(Launcher_LAYOUT_REAL "win-bundle") + else() + message(FATAL_ERROR "Cannot choose a sensible install layout for your platform.") + endif() +else() + set(Launcher_LAYOUT_REAL ${Launcher_LAYOUT}) +endif() + +if(Launcher_LAYOUT_REAL STREQUAL "mac-bundle") + set(BINARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") + set(LIBRARY_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") + set(PLUGIN_DEST_DIR "${Launcher_Name}.app/Contents/MacOS") + set(RESOURCES_DEST_DIR "${Launcher_Name}.app/Contents/Resources") + set(JARS_DEST_DIR "${Launcher_Name}.app/Contents/MacOS/jars") + + set(BUNDLE_DEST_DIR ".") + + # Apps to bundle + set(APPS "\${CMAKE_INSTALL_PREFIX}/${Launcher_Name}.app") + + # Mac bundle settings + set(MACOSX_BUNDLE_BUNDLE_NAME "${Launcher_Name}") + set(MACOSX_BUNDLE_INFO_STRING "${Launcher_Name}: Minecraft launcher and management utility.") + set(MACOSX_BUNDLE_GUI_IDENTIFIER "org.multimc.${Launcher_Name}") + set(MACOSX_BUNDLE_BUNDLE_VERSION "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}") + set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}") + set(MACOSX_BUNDLE_LONG_VERSION_STRING "${Launcher_VERSION_MAJOR}.${Launcher_VERSION_MINOR}.${Launcher_VERSION_HOTFIX}.${Launcher_VERSION_BUILD}") + set(MACOSX_BUNDLE_ICON_FILE ${Launcher_Name}.icns) + set(MACOSX_BUNDLE_COPYRIGHT "Copyright 2015-2023 ${Launcher_Copyright}") + + # directories to look for dependencies + set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + + # install as bundle + set(INSTALL_BUNDLE "full") + + # Add the icon + install(FILES ${Launcher_Branding_ICNS} DESTINATION ${RESOURCES_DEST_DIR} RENAME ${Launcher_Name}.icns) + +elseif(Launcher_LAYOUT_REAL STREQUAL "lin-nodeps") + set(BINARY_DEST_DIR "bin") + set(LIBRARY_DEST_DIR "bin") + set(PLUGIN_DEST_DIR "plugins") + set(BUNDLE_DEST_DIR ".") + set(RESOURCES_DEST_DIR ".") + set(JARS_DEST_DIR "bin/jars") + + # install as bundle with no dependencies included + set(INSTALL_BUNDLE "nodeps") + + # Set RPATH + SET(Launcher_BINARY_RPATH "$ORIGIN/") + + # Install basic runner script + configure_file(launcher/Launcher.in "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" @ONLY) + install(PROGRAMS "${CMAKE_CURRENT_BINARY_DIR}/LauncherScript" DESTINATION ${BUNDLE_DEST_DIR} RENAME ${Launcher_Name}) + +elseif(Launcher_LAYOUT_REAL STREQUAL "win-bundle") + set(BINARY_DEST_DIR ".") + set(LIBRARY_DEST_DIR ".") + set(PLUGIN_DEST_DIR ".") + set(BUNDLE_DEST_DIR ".") + set(RESOURCES_DEST_DIR ".") + set(JARS_DEST_DIR "jars") + + # Apps to bundle + set(APPS "\${CMAKE_INSTALL_PREFIX}/${Launcher_Name}.exe") + + # directories to look for dependencies + set(DIRS ${QT_LIBS_DIR} ${QT_LIBEXECS_DIR} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + + # install as bundle + set(INSTALL_BUNDLE "full") +else() + message(FATAL_ERROR "No sensible install layout set.") +endif() + +################################ Included Libs ################################ + +include(ExternalProject) +set_directory_properties(PROPERTIES EP_BASE External) + +option(NBT_BUILD_SHARED "Build NBT shared library" ON) +option(NBT_USE_ZLIB "Build NBT library with zlib support" OFF) +option(NBT_BUILD_TESTS "Build NBT library tests" OFF) #FIXME: fix unit tests. +set(NBT_NAME Launcher_nbt++) +set(NBT_DEST_DIR ${LIBRARY_DEST_DIR}) +add_subdirectory(libraries/libnbtplusplus) + +add_subdirectory(libraries/ganalytics) # google analytics library +add_subdirectory(libraries/systeminfo) # system information library +add_subdirectory(libraries/hoedown) # markdown parser +add_subdirectory(libraries/launcher) # java based launcher part for Minecraft +add_subdirectory(libraries/javacheck) # java compatibility checker +add_subdirectory(libraries/xz-embedded) # xz compression +add_subdirectory(libraries/quazip) # zip manipulation library +add_subdirectory(libraries/rainbow) # Qt extension for colors +add_subdirectory(libraries/iconfix) # fork of Qt's QIcon loader +add_subdirectory(libraries/LocalPeer) # fork of a library from Qt solutions +add_subdirectory(libraries/classparser) # google analytics library +add_subdirectory(libraries/optional-bare) +add_subdirectory(libraries/tomlc99) # toml parser +add_subdirectory(libraries/katabasis) # An OAuth2 library that tried to do too much + +############################### Built Artifacts ############################### + +add_subdirectory(buildconfig) + +# NOTE: this must always be last to appease the CMake deity of quirky install command evaluation order. +add_subdirectory(launcher) diff --git a/ultimmc/COPYING.md b/ultimmc/COPYING.md new file mode 100644 index 0000000..0cbe6ed --- /dev/null +++ b/ultimmc/COPYING.md @@ -0,0 +1,367 @@ +# MultiMC + +Portions are licensed under Apache 2.0 License: + + Copyright 2012-2021 MultiMC Contributors + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +Portions are licensed under MS-PL: + + This license governs use of the accompanying software. If you use the + software, you accept this license. If you do not accept the license, + do not use the software. + + 1. Definitions + The terms "reproduce," "reproduction," "derivative works," and + "distribution" have the same meaning here as under U.S. copyright law. + + A "contribution" is the original software, or any additions or + changes to the software. + + A "contributor" is any person that distributes its contribution + under this license. + + "Licensed patents" are a contributor's patent claims that read + directly on its contribution. + + 2. Grant of Rights + + (A) Copyright Grant- Subject to the terms of this license, + including the license conditions and limitations in section 3, + each contributor grants you a non-exclusive, worldwide, royalty-free + copyright license to reproduce its contribution, prepare derivative + works of its contribution, and distribute its contribution or any + derivative works that you create. + + (B) Patent Grant- Subject to the terms of this license, including + the license conditions and limitations in section 3, each contributor + grants you a non-exclusive, worldwide, royalty-free license under its + licensed patents to make, have made, use, sell, offer for sale, import, + and/or otherwise dispose of its contribution in the software or derivative + works of the contribution in the software. + + 3. Conditions and Limitations + + (A) No Trademark License- This license does not grant you rights to + use any contributors' name, logo, or trademarks. + + (B) If you bring a patent claim against any contributor over patents + that you claim are infringed by the software, your patent license + from such contributor to the software ends automatically. + + (C) If you distribute any portion of the software, you must retain all + copyright, patent, trademark, and attribution notices that are present + in the software. + + (D) If you distribute any portion of the software in source code form, + you may do so only under this license by including a complete copy of + this license with your distribution. If you distribute any portion of + the software in compiled or object code form, you may only do so under + a license that complies with this license. + + (E) The software is licensed "as-is." You bear the risk of using it. + The contributors give no express warranties, guarantees or conditions. + You may have additional consumer rights under your local laws which + this license cannot change. To the extent permitted under your local + laws, the contributors exclude the implied warranties of merchantability, + fitness for a particular purpose and non-infringement. + +# MinGW runtime (Windows) + + Copyright (c) 2012 MinGW.org project + + 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: + + The above copyright notice, this permission notice and the below disclaimer + shall be included in all copies or substantial portions of the Software. + + 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. + +# Qt 5 + + Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). + Contact: http://www.qt-project.org/legal + + Licensed under LGPL v2.1 + +# libnbt++ + + libnbt++ - A library for the Minecraft Named Binary Tag format. + Copyright (C) 2013, 2015 ljfa-ag + + libnbt++ is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + libnbt++ is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with libnbt++. If not, see . + +# rainbow (KGuiAddons) + + Copyright (C) 2007 Matthew Woehlke + Copyright (C) 2007 Olaf Schmidt + Copyright (C) 2007 Thomas Zander + Copyright (C) 2007 Zack Rusin + Copyright (C) 2015 Petr Mrazek + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. + +# Hoedown + + Copyright (c) 2008, Natacha Porté + Copyright (c) 2011, Vicent Martí + Copyright (c) 2014, Xavier Mendez, Devin Torres and the Hoedown authors + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +# Batch icon set + + You are free to use Batch (the "icon set") or any part thereof (the "icons") + in any personal, open-source or commercial work without obligation of payment + (monetary or otherwise) or attribution. Do not sell the icon set, host + the icon set or rent the icon set (either in existing or modified form). + + While attribution is optional, it is always appreciated. + + Intellectual property rights are not transferred with the download of the icons. + + EXCEPT TO THE EXTENT REQUIRED BY APPLICABLE LAW, IN NO EVENT WILL ADAM WHITCROFT + BE LIABLE TO YOU ON ANY LEGAL THEORY FOR ANY SPECIAL, INCIDENTAL, CONSEQUENTIAL, + PUNITIVE OR EXEMPLARY DAMAGES ARISING OUT OF THE USE OF THE ICONS, + EVEN IF LICENSOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +# Material Design Icons + + Copyright (c) 2014, Austin Andrews (http://materialdesignicons.com/), + with Reserved Font Name Material Design Icons. + Copyright (c) 2014, Google (http://www.google.com/design/) + uses the license at https://github.com/google/material-design-icons/blob/master/LICENSE + + This Font Software is licensed under the SIL Open Font License, Version 1.1. + This license is copied below, and is also available with a FAQ at: + http://scripts.sil.org/OFL + +# Quazip + + Copyright (C) 2005-2011 Sergey A. Tachenov + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at + your option) any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + See COPYING file for the full LGPL text. + + Original ZIP package is copyrighted by Gilles Vollant, see + quazip/(un)zip.h files for details, basically it's zlib license. + +# xz-minidec + + XZ decompressor + + Authors: Lasse Collin + Igor Pavlov + + This file has been put into the public domain. + You can do whatever you want with this file. + +# ColumnResizer + + Copyright (c) 2011-2016 Aurélien Gâteau and contributors. + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted (subject to the limitations in the + disclaimer below) provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the + distribution. + + * The name of the contributors may not be used to endorse or + promote products derived from this software without specific prior + written permission. + + NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE + GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT + HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED + WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# lionshead + + Code has been taken from https://github.com/natefoo/lionshead and loosely + translated to C++ laced with Qt. + + MIT License + + Copyright (c) 2017 Nate Coraor + + 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: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + 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. + +# optional-bare + + Code from https://github.com/martinmoene/optional-bare/ + + Boost Software License - Version 1.0 - August 17th, 2003 + + Permission is hereby granted, free of charge, to any person or organization + obtaining a copy of the software and accompanying documentation covered by + this license (the "Software") to use, reproduce, display, distribute, + execute, and transmit the Software, and to prepare derivative works of the + Software, and to permit third-parties to whom the Software is furnished to + do so, all subject to the following: + + The copyright notices in the Software and this entire statement, including + the above license grant, this restriction and the following disclaimer, + must be included in all copies of the Software, in whole or in part, and + all derivative works of the Software, unless such copies or derivative + works are solely in the form of machine-executable object code generated by + a source language processor. + + 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, TITLE AND NON-INFRINGEMENT. IN NO EVENT + SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE + FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, + ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + +# tomlc99 + + MIT License + + Copyright (c) 2017 CK Tan + https://github.com/cktan/tomlc99 + + 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: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + 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. + +# O2 (Katabasis fork) + + Copyright (c) 2012, Akos Polster + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ultimmc/README.md b/ultimmc/README.md new file mode 100644 index 0000000..22fba04 --- /dev/null +++ b/ultimmc/README.md @@ -0,0 +1,61 @@ +

+ + + +

UltimMC is a custom launcher for Minecraft which allows you to manage multiple instances and use offline ("cracked") accounts while keeping as close as possible to the original.

+ +

+ +> [!IMPORTANT] +> This project is a **fork** of MultiMC.

+> This software is provided without any warranty, so please don't contact the main +> MultiMC developers in case anything goes wrong using this launcher.

+> Nonetheless, feel free to create an issue within this repository +> in case you face an issue specific to UltimMC. + +## Downloading + +- All the available downloads can be found [here](https://nightly.link/UltimMC/Launcher/workflows/main/develop). These builds are directly taken from our [GitHub Actions](https://github.com/UltimMC/Launcher/actions). + +Direct downloads for specific platforms can be found below. + +- *[Windows \(32-bit and 64-bit\)](https://nightly.link/UltimMC/Launcher/workflows/main/develop/mmc-cracked-win32.zip)*. + +- *[Linux (64-bit)](https://nightly.link/UltimMC/Launcher/workflows/main/develop/mmc-cracked-lin64.zip)*. + +- *[macOS (10.14 and newer)](https://nightly.link/UltimMC/Launcher/workflows/main/develop/mmc-cracked-osx64.zip)*. + +> [!NOTE] +> In the case you're using macOS then another additional step you might need to do +> is to make `UltimMC` an executable by running the command `chmod +x UltimMC.app/Contents/MacOS/UltimMC` in the terminal. + +There's additionally a [.deb package](https://nightly.link/UltimMC/ultimmc-deb/workflows/ci/master/UltimMC.zip) for Debian/Ubuntu distributions. + +And an AUR package as [ultimmc-bin](https://aur.archlinux.org/packages/ultimmc-bin). [![ultimmc-bin](https://img.shields.io/badge/ultimmc--bin-1793D1?logo=archlinux&logoColor=white&label=AUR)](https://aur.archlinux.org/packages/ultimmc-bin) + +## Installing and Using + +1. Pick the correct download for your system. +2. Uncompress it in your desired directory. +3. Launch `UltimMC`. +4. Go to account settings. +6. A. Pick "Add Local" and you will be requested to use the username you desire, this can be anything. +7. B. Pick "Add Ely.by" and add your Ely.by account by putting your email and password. +8. Save it. +9. Now enjoy the Launcher. + +## Updating + +To update the launcher replace all replaceable files and folders with the newer ones from any of the links listed above. + +A better update system is in the works. + +## Forking + +This project now includes our MSA API key in order to have functional Microsoft authentication within the launcher. + +This means you're accepting the: + +- [Microsoft Identity Platform Terms of Use](https://learn.microsoft.com/en-us/legal/microsoft-identity-platform/terms-of-use) + +We humbly ask that in case you wish to fork UltimMC, please either remove the key by setting it empty (`""`) or by setting your own. diff --git a/ultimmc/buildconfig/BuildConfig.cpp.in b/ultimmc/buildconfig/BuildConfig.cpp.in new file mode 100644 index 0000000..6fa4979 --- /dev/null +++ b/ultimmc/buildconfig/BuildConfig.cpp.in @@ -0,0 +1,72 @@ +#include "BuildConfig.h" +#include + +const Config BuildConfig; + +Config::Config() +{ + // Name and copyright + LAUNCHER_NAME = "@Launcher_Name@"; + LAUNCHER_DISPLAYNAME = "@Launcher_DisplayName@"; + LAUNCHER_COPYRIGHT = "@Launcher_Copyright@"; + LAUNCHER_DOMAIN = "@Launcher_Domain@"; + LAUNCHER_CONFIGFILE = "@Launcher_ConfigFile@"; + LAUNCHER_GIT = "@Launcher_Git@"; + + USER_AGENT = "@Launcher_UserAgent@"; + USER_AGENT_UNCACHED = USER_AGENT + " (Uncached)"; + + // Version information + VERSION_MAJOR = @Launcher_VERSION_MAJOR@; + VERSION_MINOR = @Launcher_VERSION_MINOR@; + VERSION_HOTFIX = @Launcher_VERSION_HOTFIX@; + VERSION_BUILD = @Launcher_VERSION_BUILD@; + + BUILD_PLATFORM = "@Launcher_BUILD_PLATFORM@"; + UPDATER_BASE = "@Launcher_UPDATER_BASE@"; + ANALYTICS_ID = "@Launcher_ANALYTICS_ID@"; + NOTIFICATION_URL = "@Launcher_NOTIFICATION_URL@"; + FULL_VERSION_STR = "@Launcher_VERSION_MAJOR@.@Launcher_VERSION_MINOR@.@Launcher_VERSION_BUILD@"; + + GIT_COMMIT = "@Launcher_GIT_COMMIT@"; + GIT_REFSPEC = "@Launcher_GIT_REFSPEC@"; + if(GIT_REFSPEC.startsWith("refs/heads/") && !UPDATER_BASE.isEmpty() && !BUILD_PLATFORM.isEmpty() && VERSION_BUILD >= 0) + { + VERSION_CHANNEL = GIT_REFSPEC; + VERSION_CHANNEL.remove("refs/heads/"); + UPDATER_ENABLED = true; + } + else + { + VERSION_CHANNEL = QObject::tr("custom"); + } + + VERSION_STR = "@Launcher_VERSION_STRING@"; + NEWS_RSS_URL = "@Launcher_NEWS_RSS_URL@"; + PASTE_EE_KEY = "@Launcher_PASTE_EE_API_KEY@"; + IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@"; + MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@"; + META_URL = "@Launcher_META_URL@"; + + BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@"; + DISCORD_URL = "@Launcher_DISCORD_URL@"; + SUBREDDIT_URL = "@Launcher_SUBREDDIT_URL@"; +} + +QString Config::printableVersionString() const +{ + QString vstr = QString("%1.%2.%3").arg(QString::number(VERSION_MAJOR), QString::number(VERSION_MINOR), QString::number(VERSION_HOTFIX)); + + // If the build is not a main release, append the channel + if(VERSION_CHANNEL != "develop") + { + vstr += "-" + VERSION_CHANNEL; + } + + // if a build number is set, also add it to the end + if(VERSION_BUILD >= 0) + { + vstr += "-" + QString::number(VERSION_BUILD); + } + return vstr; +} diff --git a/ultimmc/buildconfig/BuildConfig.h b/ultimmc/buildconfig/BuildConfig.h new file mode 100644 index 0000000..589aafe --- /dev/null +++ b/ultimmc/buildconfig/BuildConfig.h @@ -0,0 +1,121 @@ +#pragma once +#include + +/** + * \brief The Config class holds all the build-time information passed from the build system. + */ +class Config +{ +public: + Config(); + QString LAUNCHER_NAME; + QString LAUNCHER_DISPLAYNAME; + QString LAUNCHER_COPYRIGHT; + QString LAUNCHER_DOMAIN; + QString LAUNCHER_CONFIGFILE; + QString LAUNCHER_GIT; + + /// The major version number. + int VERSION_MAJOR; + /// The minor version number. + int VERSION_MINOR; + /// The hotfix number. + int VERSION_HOTFIX; + /// The build number. + int VERSION_BUILD; + + /** + * The version channel + * This is used by the updater to determine what channel the current version came from. + */ + QString VERSION_CHANNEL; + + bool UPDATER_ENABLED = false; + + /// A short string identifying this build's platform. For example, "lin64" or "win32". + QString BUILD_PLATFORM; + + /// URL for the updater's channel + QString UPDATER_BASE; + + + /// User-Agent to use. + QString USER_AGENT; + + /// User-Agent to use for uncached requests. + QString USER_AGENT_UNCACHED; + + + /// Google analytics ID + QString ANALYTICS_ID; + + /// URL for notifications + QString NOTIFICATION_URL; + + /// Used for matching notifications + QString FULL_VERSION_STR; + + /// The git commit hash of this build + QString GIT_COMMIT; + + /// The git refspec of this build + QString GIT_REFSPEC; + + /// This is printed on start to standard output + QString VERSION_STR; + + /** + * This is used to fetch the news RSS feed. + * It defaults in CMakeLists.txt to "https://multimc.org/rss.xml" + */ + QString NEWS_RSS_URL; + + /** + * API key you can get from paste.ee when you register an account + */ + QString PASTE_EE_KEY; + + /** + * Client ID you can get from Imgur when you register an application + */ + QString IMGUR_CLIENT_ID; + + /** + * Client ID you can get from Microsoft Identity Platform when you register an application + */ + QString MSA_CLIENT_ID; + + /** + * Metadata repository URL prefix + */ + QString META_URL; + + QString BUG_TRACKER_URL; + QString DISCORD_URL; + QString SUBREDDIT_URL; + + QString RESOURCE_BASE = "https://resources.download.minecraft.net/"; + QString LIBRARY_BASE = "https://libraries.minecraft.net/"; + QString AUTH_BASE = "https://authserver.mojang.com/"; + QString IMGUR_BASE_URL = "https://api.imgur.com/3/"; + QString FMLLIBS_BASE_URL = "https://files.multimc.org/fmllibs/"; + QString TRANSLATIONS_BASE_URL = "https://files.multimc.org/translations/"; + + QString LEGACY_FTB_CDN_BASE_URL = "https://dist.creeper.host/FTB2/"; + + QString ATL_DOWNLOAD_SERVER_URL = "https://download.nodecdn.net/containers/atl/"; + + QString TECHNIC_API_BASE_URL = "https://api.technicpack.net/"; + /** + * The build that is reported to the Technic API. + */ + QString TECHNIC_API_BUILD = "multimc"; + + /** + * \brief Converts the Version to a string. + * \return The version number in string format (major.minor.revision.build). + */ + QString printableVersionString() const; +}; + +extern const Config BuildConfig; diff --git a/ultimmc/buildconfig/CMakeLists.txt b/ultimmc/buildconfig/CMakeLists.txt new file mode 100644 index 0000000..de4fd35 --- /dev/null +++ b/ultimmc/buildconfig/CMakeLists.txt @@ -0,0 +1,11 @@ +######## Configure the file with build properties ######## + +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/BuildConfig.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp") + +add_library(BuildConfig STATIC + BuildConfig.h + ${CMAKE_CURRENT_BINARY_DIR}/BuildConfig.cpp +) + +target_link_libraries(BuildConfig Qt5::Core) +target_include_directories(BuildConfig PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/ultimmc/cmake/BundleUtilities.cmake b/ultimmc/cmake/BundleUtilities.cmake new file mode 100644 index 0000000..e3f50b9 --- /dev/null +++ b/ultimmc/cmake/BundleUtilities.cmake @@ -0,0 +1,786 @@ +# - Functions to help assemble a standalone bundle application. +# A collection of CMake utility functions useful for dealing with .app +# bundles on the Mac and bundle-like directories on any OS. +# +# The following functions are provided by this module: +# fixup_bundle +# copy_and_fixup_bundle +# verify_app +# get_bundle_main_executable +# get_dotapp_dir +# get_bundle_and_executable +# get_bundle_all_executables +# get_item_key +# clear_bundle_keys +# set_bundle_key_values +# get_bundle_keys +# copy_resolved_item_into_bundle +# copy_resolved_framework_into_bundle +# fixup_bundle_item +# verify_bundle_prerequisites +# verify_bundle_symlinks +# Requires CMake 2.6 or greater because it uses function, break and +# PARENT_SCOPE. Also depends on GetPrerequisites.cmake. +# +# FIXUP_BUNDLE( ) +# Fix up a bundle in-place and make it standalone, such that it can be +# drag-n-drop copied to another machine and run on that machine as long as all +# of the system libraries are compatible. +# +# If you pass plugins to fixup_bundle as the libs parameter, you should install +# them or copy them into the bundle before calling fixup_bundle. The "libs" +# parameter is a list of libraries that must be fixed up, but that cannot be +# determined by otool output analysis. (i.e., plugins) +# +# Gather all the keys for all the executables and libraries in a bundle, and +# then, for each key, copy each prerequisite into the bundle. Then fix each one +# up according to its own list of prerequisites. +# +# Then clear all the keys and call verify_app on the final bundle to ensure +# that it is truly standalone. +# +# COPY_AND_FIXUP_BUNDLE( ) +# Makes a copy of the bundle at location and then fixes up the +# new copied bundle in-place at ... +# +# VERIFY_APP() +# Verifies that an application appears valid based on running analysis +# tools on it. Calls "message(FATAL_ERROR" if the application is not verified. +# +# GET_BUNDLE_MAIN_EXECUTABLE( ) +# The result will be the full path name of the bundle's main executable file +# or an "error:" prefixed string if it could not be determined. +# +# GET_DOTAPP_DIR( ) +# Returns the nearest parent dir whose name ends with ".app" given the full +# path to an executable. If there is no such parent dir, then simply return +# the dir containing the executable. +# +# The returned directory may or may not exist. +# +# GET_BUNDLE_AND_EXECUTABLE( ) +# Takes either a ".app" directory name or the name of an executable +# nested inside a ".app" directory and returns the path to the ".app" +# directory in and the path to its main executable in +# +# +# GET_BUNDLE_ALL_EXECUTABLES( ) +# Scans the given bundle recursively for all executable files and accumulates +# them into a variable. +# +# GET_ITEM_KEY( ) +# Given a file (item) name, generate a key that should be unique considering +# the set of libraries that need copying or fixing up to make a bundle +# standalone. This is essentially the file name including extension with "." +# replaced by "_" +# +# This key is used as a prefix for CMake variables so that we can associate a +# set of variables with a given item based on its key. +# +# CLEAR_BUNDLE_KEYS() +# Loop over the list of keys, clearing all the variables associated with each +# key. After the loop, clear the list of keys itself. +# +# Caller of get_bundle_keys should call clear_bundle_keys when done with list +# of keys. +# +# SET_BUNDLE_KEY_VALUES( +# ) +# Add a key to the list (if necessary) for the given item. If added, +# also set all the variables associated with that key. +# +# GET_BUNDLE_KEYS( ) +# Loop over all the executable and library files within the bundle (and given +# as extra ) and accumulate a list of keys representing them. Set +# values associated with each key such that we can loop over all of them and +# copy prerequisite libs into the bundle and then do appropriate +# install_name_tool fixups. +# +# COPY_RESOLVED_ITEM_INTO_BUNDLE( ) +# Copy a resolved item into the bundle if necessary. Copy is not necessary if +# the resolved_item is "the same as" the resolved_embedded_item. +# +# COPY_RESOLVED_FRAMEWORK_INTO_BUNDLE( ) +# Copy a resolved framework into the bundle if necessary. Copy is not necessary +# if the resolved_item is "the same as" the resolved_embedded_item. +# +# By default, BU_COPY_FULL_FRAMEWORK_CONTENTS is not set. If you want full +# frameworks embedded in your bundles, set BU_COPY_FULL_FRAMEWORK_CONTENTS to +# ON before calling fixup_bundle. By default, +# COPY_RESOLVED_FRAMEWORK_INTO_BUNDLE copies the framework dylib itself plus +# the framework Resources directory. +# +# FIXUP_BUNDLE_ITEM( ) +# Get the direct/non-system prerequisites of the resolved embedded item. For +# each prerequisite, change the way it is referenced to the value of the +# _EMBEDDED_ITEM keyed variable for that prerequisite. (Most likely changing to +# an "@executable_path" style reference.) +# +# This function requires that the resolved_embedded_item be "inside" the bundle +# already. In other words, if you pass plugins to fixup_bundle as the libs +# parameter, you should install them or copy them into the bundle before +# calling fixup_bundle. The "libs" parameter is a list of libraries that must +# be fixed up, but that cannot be determined by otool output analysis. (i.e., +# plugins) +# +# Also, change the id of the item being fixed up to its own _EMBEDDED_ITEM +# value. +# +# Accumulate changes in a local variable and make *one* call to +# install_name_tool at the end of the function with all the changes at once. +# +# If the BU_CHMOD_BUNDLE_ITEMS variable is set then bundle items will be +# marked writable before install_name_tool tries to change them. +# +# VERIFY_BUNDLE_PREREQUISITES( ) +# Verifies that the sum of all prerequisites of all files inside the bundle +# are contained within the bundle or are "system" libraries, presumed to exist +# everywhere. +# +# VERIFY_BUNDLE_SYMLINKS( ) +# Verifies that any symlinks found in the bundle point to other files that are +# already also in the bundle... Anything that points to an external file causes +# this function to fail the verification. + +#============================================================================= +# Copyright 2008-2009 Kitware, Inc. +# +# Distributed under the OSI-approved BSD License (the "License"); +# see accompanying file Copyright.txt for details. +# +# This software is distributed WITHOUT ANY WARRANTY; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the License for more information. +#============================================================================= +# (To distribute this file outside of CMake, substitute the full +# License text for the above reference.) + +# The functions defined in this file depend on the get_prerequisites function +# (and possibly others) found in: +# +get_filename_component(BundleUtilities_cmake_dir "${CMAKE_CURRENT_LIST_FILE}" PATH) +include("${BundleUtilities_cmake_dir}/GetPrerequisites.cmake") + + +function(get_bundle_main_executable bundle result_var) + set(result "error: '${bundle}/Contents/Info.plist' file does not exist") + + if(EXISTS "${bundle}/Contents/Info.plist") + set(result "error: no CFBundleExecutable in '${bundle}/Contents/Info.plist' file") + set(line_is_main_executable 0) + set(bundle_executable "") + + # Read Info.plist as a list of lines: + # + set(eol_char "E") + file(READ "${bundle}/Contents/Info.plist" info_plist) + string(REGEX REPLACE ";" "\\\\;" info_plist "${info_plist}") + string(REGEX REPLACE "\n" "${eol_char};" info_plist "${info_plist}") + + # Scan the lines for "CFBundleExecutable" - the line after that + # is the name of the main executable. + # + foreach(line ${info_plist}) + if(line_is_main_executable) + string(REGEX REPLACE "^.*(.*).*$" "\\1" bundle_executable "${line}") + break() + endif() + + if(line MATCHES "^.*CFBundleExecutable.*$") + set(line_is_main_executable 1) + endif() + endforeach() + + if(NOT "${bundle_executable}" STREQUAL "") + if(EXISTS "${bundle}/Contents/MacOS/${bundle_executable}") + set(result "${bundle}/Contents/MacOS/${bundle_executable}") + else() + + # Ultimate goal: + # If not in "Contents/MacOS" then scan the bundle for matching files. If + # there is only one executable file that matches, then use it, otherwise + # it's an error... + # + #file(GLOB_RECURSE file_list "${bundle}/${bundle_executable}") + + # But for now, pragmatically, it's an error. Expect the main executable + # for the bundle to be in Contents/MacOS, it's an error if it's not: + # + set(result "error: '${bundle}/Contents/MacOS/${bundle_executable}' does not exist") + endif() + endif() + else() + # + # More inclusive technique... (This one would work on Windows and Linux + # too, if a developer followed the typical Mac bundle naming convention...) + # + # If there is no Info.plist file, try to find an executable with the same + # base name as the .app directory: + # + endif() + + set(${result_var} "${result}" PARENT_SCOPE) +endfunction() + + +function(get_dotapp_dir exe dotapp_dir_var) + set(s "${exe}") + + if(s MATCHES "^.*/.*\\.app/.*$") + # If there is a ".app" parent directory, + # ascend until we hit it: + # (typical of a Mac bundle executable) + # + set(done 0) + while(NOT ${done}) + get_filename_component(snamewe "${s}" NAME_WE) + get_filename_component(sname "${s}" NAME) + get_filename_component(sdir "${s}" PATH) + set(s "${sdir}") + if(sname MATCHES "\\.app$") + set(done 1) + set(dotapp_dir "${sdir}/${sname}") + endif() + endwhile() + else() + # Otherwise use a directory containing the exe + # (typical of a non-bundle executable on Mac, Windows or Linux) + # + is_file_executable("${s}" is_executable) + if(is_executable) + get_filename_component(sdir "${s}" PATH) + set(dotapp_dir "${sdir}") + else() + set(dotapp_dir "${s}") + endif() + endif() + + + set(${dotapp_dir_var} "${dotapp_dir}" PARENT_SCOPE) +endfunction() + + +function(get_bundle_and_executable app bundle_var executable_var valid_var) + set(valid 0) + + if(EXISTS "${app}") + # Is it a directory ending in .app? + if(IS_DIRECTORY "${app}") + if(app MATCHES "\\.app$") + get_bundle_main_executable("${app}" executable) + if(EXISTS "${app}" AND EXISTS "${executable}") + set(${bundle_var} "${app}" PARENT_SCOPE) + set(${executable_var} "${executable}" PARENT_SCOPE) + set(valid 1) + #message(STATUS "info: handled .app directory case...") + else() + message(STATUS "warning: *NOT* handled - .app directory case...") + endif() + else() + message(STATUS "warning: *NOT* handled - directory but not .app case...") + endif() + else() + # Is it an executable file? + is_file_executable("${app}" is_executable) + if(is_executable) + get_dotapp_dir("${app}" dotapp_dir) + if(EXISTS "${dotapp_dir}") + set(${bundle_var} "${dotapp_dir}" PARENT_SCOPE) + set(${executable_var} "${app}" PARENT_SCOPE) + set(valid 1) + #message(STATUS "info: handled executable file in .app dir case...") + else() + get_filename_component(app_dir "${app}" PATH) + set(${bundle_var} "${app_dir}" PARENT_SCOPE) + set(${executable_var} "${app}" PARENT_SCOPE) + set(valid 1) + #message(STATUS "info: handled executable file in any dir case...") + endif() + else() + message(STATUS "warning: *NOT* handled - not .app dir, not executable file...") + endif() + endif() + else() + message(STATUS "warning: *NOT* handled - directory/file ${app} does not exist...") + endif() + + if(NOT valid) + set(${bundle_var} "error: not a bundle" PARENT_SCOPE) + set(${executable_var} "error: not a bundle" PARENT_SCOPE) + endif() + + set(${valid_var} ${valid} PARENT_SCOPE) +endfunction() + + +function(get_bundle_all_executables bundle exes_var) + set(exes "") + + file(GLOB_RECURSE file_list "${bundle}/*") + foreach(f ${file_list}) + is_file_executable("${f}" is_executable) + if(is_executable) + set(exes ${exes} "${f}") + endif() + endforeach() + + set(${exes_var} "${exes}" PARENT_SCOPE) +endfunction() + + +function(get_item_key item key_var) + get_filename_component(item_name "${item}" NAME) + if(WIN32) + string(TOLOWER "${item_name}" item_name) + endif() + string(REGEX REPLACE "\\." "_" ${key_var} "${item_name}") + set(${key_var} ${${key_var}} PARENT_SCOPE) +endfunction() + + +function(clear_bundle_keys keys_var) + foreach(key ${${keys_var}}) + set(${key}_ITEM PARENT_SCOPE) + set(${key}_RESOLVED_ITEM PARENT_SCOPE) + set(${key}_DEFAULT_EMBEDDED_PATH PARENT_SCOPE) + set(${key}_EMBEDDED_ITEM PARENT_SCOPE) + set(${key}_RESOLVED_EMBEDDED_ITEM PARENT_SCOPE) + set(${key}_COPYFLAG PARENT_SCOPE) + endforeach() + set(${keys_var} PARENT_SCOPE) +endfunction() + + +function(set_bundle_key_values keys_var context item exepath dirs copyflag) + get_filename_component(item_name "${item}" NAME) + + get_item_key("${item}" key) + + list(LENGTH ${keys_var} length_before) + gp_append_unique(${keys_var} "${key}") + list(LENGTH ${keys_var} length_after) + + if(NOT length_before EQUAL length_after) + gp_resolve_item("${context}" "${item}" "${exepath}" "${dirs}" resolved_item) + + gp_item_default_embedded_path("${item}" default_embedded_path) + + if(item MATCHES "[^/]+\\.framework/") + # For frameworks, construct the name under the embedded path from the + # opening "${item_name}.framework/" to the closing "/${item_name}": + # + string(REGEX REPLACE "^.*(${item_name}.framework/.*/?${item_name}).*$" "${default_embedded_path}/\\1" embedded_item "${item}") + else() + # For other items, just use the same name as the original, but in the + # embedded path: + # + set(embedded_item "${default_embedded_path}/${item_name}") + endif() + + # Replace @executable_path and resolve ".." references: + # + string(REPLACE "@executable_path" "${exepath}" resolved_embedded_item "${embedded_item}") + get_filename_component(resolved_embedded_item "${resolved_embedded_item}" ABSOLUTE) + + # *But* -- if we are not copying, then force resolved_embedded_item to be + # the same as resolved_item. In the case of multiple executables in the + # original bundle, using the default_embedded_path results in looking for + # the resolved executable next to the main bundle executable. This is here + # so that exes in the other sibling directories (like "bin") get fixed up + # properly... + # + if(NOT copyflag) + set(resolved_embedded_item "${resolved_item}") + endif() + + set(${keys_var} ${${keys_var}} PARENT_SCOPE) + set(${key}_ITEM "${item}" PARENT_SCOPE) + set(${key}_RESOLVED_ITEM "${resolved_item}" PARENT_SCOPE) + set(${key}_DEFAULT_EMBEDDED_PATH "${default_embedded_path}" PARENT_SCOPE) + set(${key}_EMBEDDED_ITEM "${embedded_item}" PARENT_SCOPE) + set(${key}_RESOLVED_EMBEDDED_ITEM "${resolved_embedded_item}" PARENT_SCOPE) + set(${key}_COPYFLAG "${copyflag}" PARENT_SCOPE) + else() + #message("warning: item key '${key}' already in the list, subsequent references assumed identical to first") + endif() +endfunction() + + +function(get_bundle_keys app libs dirs keys_var) + set(${keys_var} PARENT_SCOPE) + + get_bundle_and_executable("${app}" bundle executable valid) + if(valid) + # Always use the exepath of the main bundle executable for @executable_path + # replacements: + # + get_filename_component(exepath "${executable}" PATH) + + # But do fixups on all executables in the bundle: + # + get_bundle_all_executables("${bundle}" exes) + + # For each extra lib, accumulate a key as well and then also accumulate + # any of its prerequisites. (Extra libs are typically dynamically loaded + # plugins: libraries that are prerequisites for full runtime functionality + # but that do not show up in otool -L output...) + # + foreach(lib ${libs}) + set_bundle_key_values(${keys_var} "${lib}" "${lib}" "${exepath}" "${dirs}" 0) + + set(prereqs "") + get_prerequisites("${lib}" prereqs 1 1 "${exepath}" "${dirs}") + foreach(pr ${prereqs}) + set_bundle_key_values(${keys_var} "${lib}" "${pr}" "${exepath}" "${dirs}" 1) + endforeach() + endforeach() + + # For each executable found in the bundle, accumulate keys as we go. + # The list of keys should be complete when all prerequisites of all + # binaries in the bundle have been analyzed. + # + foreach(exe ${exes}) + # Add the exe itself to the keys: + # + set_bundle_key_values(${keys_var} "${exe}" "${exe}" "${exepath}" "${dirs}" 0) + + # Add each prerequisite to the keys: + # + set(prereqs "") + get_prerequisites("${exe}" prereqs 1 1 "${exepath}" "${dirs}") + foreach(pr ${prereqs}) + set_bundle_key_values(${keys_var} "${exe}" "${pr}" "${exepath}" "${dirs}" 1) + endforeach() + endforeach() + + # Propagate values to caller's scope: + # + set(${keys_var} ${${keys_var}} PARENT_SCOPE) + foreach(key ${${keys_var}}) + set(${key}_ITEM "${${key}_ITEM}" PARENT_SCOPE) + set(${key}_RESOLVED_ITEM "${${key}_RESOLVED_ITEM}" PARENT_SCOPE) + set(${key}_DEFAULT_EMBEDDED_PATH "${${key}_DEFAULT_EMBEDDED_PATH}" PARENT_SCOPE) + set(${key}_EMBEDDED_ITEM "${${key}_EMBEDDED_ITEM}" PARENT_SCOPE) + set(${key}_RESOLVED_EMBEDDED_ITEM "${${key}_RESOLVED_EMBEDDED_ITEM}" PARENT_SCOPE) + set(${key}_COPYFLAG "${${key}_COPYFLAG}" PARENT_SCOPE) + endforeach() + endif() +endfunction() + + +function(copy_resolved_item_into_bundle resolved_item resolved_embedded_item) + if(WIN32) + # ignore case on Windows + string(TOLOWER "${resolved_item}" resolved_item_compare) + string(TOLOWER "${resolved_embedded_item}" resolved_embedded_item_compare) + else() + set(resolved_item_compare "${resolved_item}") + set(resolved_embedded_item_compare "${resolved_embedded_item}") + endif() + + if("${resolved_item_compare}" STREQUAL "${resolved_embedded_item_compare}") + message(STATUS "warning: resolved_item == resolved_embedded_item - not copying...") + else() + #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy ${resolved_item} ${resolved_embedded_item}") + execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_item}" "${resolved_embedded_item}") + if(UNIX AND NOT APPLE) + file(RPATH_REMOVE FILE "${resolved_embedded_item}") + endif() + endif() + +endfunction() + + +function(copy_resolved_framework_into_bundle resolved_item resolved_embedded_item) + if(WIN32) + # ignore case on Windows + string(TOLOWER "${resolved_item}" resolved_item_compare) + string(TOLOWER "${resolved_embedded_item}" resolved_embedded_item_compare) + else() + set(resolved_item_compare "${resolved_item}") + set(resolved_embedded_item_compare "${resolved_embedded_item}") + endif() + + if("${resolved_item_compare}" STREQUAL "${resolved_embedded_item_compare}") + message(STATUS "warning: resolved_item == resolved_embedded_item - not copying...") + else() + if(BU_COPY_FULL_FRAMEWORK_CONTENTS) + # Full Framework (everything): + get_filename_component(resolved_dir "${resolved_item}" PATH) + get_filename_component(resolved_dir "${resolved_dir}/../.." ABSOLUTE) + get_filename_component(resolved_embedded_dir "${resolved_embedded_item}" PATH) + get_filename_component(resolved_embedded_dir "${resolved_embedded_dir}/../.." ABSOLUTE) + #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy_directory '${resolved_dir}' '${resolved_embedded_dir}'") + execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${resolved_dir}" "${resolved_embedded_dir}") + else() + # Framework lib itself: + #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy ${resolved_item} ${resolved_embedded_item}") + execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${resolved_item}" "${resolved_embedded_item}") + + # Plus Resources, if they exist: + string(REGEX REPLACE "^(.*)/[^/]+/[^/]+/[^/]+$" "\\1/Resources" resolved_resources "${resolved_item}") + string(REGEX REPLACE "^(.*)/[^/]+/[^/]+/[^/]+$" "\\1/Resources" resolved_embedded_resources "${resolved_embedded_item}") + if(EXISTS "${resolved_resources}") + #message(STATUS "copying COMMAND ${CMAKE_COMMAND} -E copy_directory '${resolved_resources}' '${resolved_embedded_resources}'") + execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${resolved_resources}" "${resolved_embedded_resources}") + endif() + endif() + if(UNIX AND NOT APPLE) + file(RPATH_REMOVE FILE "${resolved_embedded_item}") + endif() + endif() + +endfunction() + + +function(fixup_bundle_item resolved_embedded_item exepath dirs) + # This item's key is "ikey": + # + get_item_key("${resolved_embedded_item}" ikey) + + # Ensure the item is "inside the .app bundle" -- it should not be fixed up if + # it is not in the .app bundle... Otherwise, we'll modify files in the build + # tree, or in other varied locations around the file system, with our call to + # install_name_tool. Make sure that doesn't happen here: + # + get_dotapp_dir("${exepath}" exe_dotapp_dir) + string(LENGTH "${exe_dotapp_dir}/" exe_dotapp_dir_length) + string(LENGTH "${resolved_embedded_item}" resolved_embedded_item_length) + set(path_too_short 0) + set(is_embedded 0) + if(${resolved_embedded_item_length} LESS ${exe_dotapp_dir_length}) + set(path_too_short 1) + endif() + if(NOT path_too_short) + string(SUBSTRING "${resolved_embedded_item}" 0 ${exe_dotapp_dir_length} item_substring) + if("${exe_dotapp_dir}/" STREQUAL "${item_substring}") + set(is_embedded 1) + endif() + endif() + if(NOT is_embedded) + message(" exe_dotapp_dir/='${exe_dotapp_dir}/'") + message(" item_substring='${item_substring}'") + message(" resolved_embedded_item='${resolved_embedded_item}'") + message("") + message("Install or copy the item into the bundle before calling fixup_bundle.") + message("Or maybe there's a typo or incorrect path in one of the args to fixup_bundle?") + message("") + message(FATAL_ERROR "cannot fixup an item that is not in the bundle...") + endif() + + set(prereqs "") + get_prerequisites("${resolved_embedded_item}" prereqs 1 0 "${exepath}" "${dirs}") + + set(changes "") + + foreach(pr ${prereqs}) + # Each referenced item's key is "rkey" in the loop: + # + get_item_key("${pr}" rkey) + + if(NOT "${${rkey}_EMBEDDED_ITEM}" STREQUAL "") + set(changes ${changes} "-change" "${pr}" "${${rkey}_EMBEDDED_ITEM}") + else() + message("warning: unexpected reference to '${pr}'") + endif() + endforeach() + + if(BU_CHMOD_BUNDLE_ITEMS) + execute_process(COMMAND chmod u+w "${resolved_embedded_item}") + endif() + + # Change this item's id and all of its references in one call + # to install_name_tool: + # + execute_process(COMMAND install_name_tool + ${changes} -id "${${ikey}_EMBEDDED_ITEM}" "${resolved_embedded_item}" + ) +endfunction() + + +function(fixup_bundle app libs dirs) + message(STATUS "fixup_bundle") + message(STATUS " app='${app}'") + message(STATUS " libs='${libs}'") + message(STATUS " dirs='${dirs}'") + + get_bundle_and_executable("${app}" bundle executable valid) + if(valid) + get_filename_component(exepath "${executable}" PATH) + + message(STATUS "fixup_bundle: preparing...") + get_bundle_keys("${app}" "${libs}" "${dirs}" keys) + + message(STATUS "fixup_bundle: copying...") + list(LENGTH keys n) + math(EXPR n ${n}*2) + + set(i 0) + foreach(key ${keys}) + math(EXPR i ${i}+1) + if(${${key}_COPYFLAG}) + message(STATUS "${i}/${n}: copying '${${key}_RESOLVED_ITEM}'") + else() + message(STATUS "${i}/${n}: *NOT* copying '${${key}_RESOLVED_ITEM}'") + endif() + + set(show_status 0) + if(show_status) + message(STATUS "key='${key}'") + message(STATUS "item='${${key}_ITEM}'") + message(STATUS "resolved_item='${${key}_RESOLVED_ITEM}'") + message(STATUS "default_embedded_path='${${key}_DEFAULT_EMBEDDED_PATH}'") + message(STATUS "embedded_item='${${key}_EMBEDDED_ITEM}'") + message(STATUS "resolved_embedded_item='${${key}_RESOLVED_EMBEDDED_ITEM}'") + message(STATUS "copyflag='${${key}_COPYFLAG}'") + message(STATUS "") + endif() + + if(${${key}_COPYFLAG}) + set(item "${${key}_ITEM}") + if(item MATCHES "[^/]+\\.framework/") + copy_resolved_framework_into_bundle("${${key}_RESOLVED_ITEM}" + "${${key}_RESOLVED_EMBEDDED_ITEM}") + else() + copy_resolved_item_into_bundle("${${key}_RESOLVED_ITEM}" + "${${key}_RESOLVED_EMBEDDED_ITEM}") + endif() + endif() + endforeach() + + message(STATUS "fixup_bundle: fixing...") + foreach(key ${keys}) + math(EXPR i ${i}+1) + if(APPLE) + message(STATUS "${i}/${n}: fixing up '${${key}_RESOLVED_EMBEDDED_ITEM}'") + fixup_bundle_item("${${key}_RESOLVED_EMBEDDED_ITEM}" "${exepath}" "${dirs}") + else() + message(STATUS "${i}/${n}: fix-up not required on this platform '${${key}_RESOLVED_EMBEDDED_ITEM}'") + endif() + endforeach() + + message(STATUS "fixup_bundle: cleaning up...") + clear_bundle_keys(keys) + + message(STATUS "fixup_bundle: verifying...") + verify_app("${app}") + else() + message(SEND_ERROR "error: fixup_bundle: not a valid bundle") + endif() + + message(STATUS "fixup_bundle: done") +endfunction() + + +function(copy_and_fixup_bundle src dst libs dirs) + execute_process(COMMAND ${CMAKE_COMMAND} -E copy_directory "${src}" "${dst}") + fixup_bundle("${dst}" "${libs}" "${dirs}") +endfunction() + + +function(verify_bundle_prerequisites bundle result_var info_var) + set(result 1) + set(info "") + set(count 0) + + get_bundle_main_executable("${bundle}" main_bundle_exe) + + file(GLOB_RECURSE file_list "${bundle}/*") + foreach(f ${file_list}) + is_file_executable("${f}" is_executable) + if(is_executable) + get_filename_component(exepath "${f}" PATH) + math(EXPR count "${count} + 1") + + message(STATUS "executable file ${count}: ${f}") + + set(prereqs "") + get_prerequisites("${f}" prereqs 1 1 "${exepath}" "") + + # On the Mac, + # "embedded" and "system" prerequisites are fine... anything else means + # the bundle's prerequisites are not verified (i.e., the bundle is not + # really "standalone") + # + # On Windows (and others? Linux/Unix/...?) + # "local" and "system" prereqs are fine... + # + set(external_prereqs "") + + foreach(p ${prereqs}) + set(p_type "") + gp_file_type("${f}" "${p}" p_type) + + if(APPLE) + if(NOT "${p_type}" STREQUAL "embedded" AND NOT "${p_type}" STREQUAL "system") + set(external_prereqs ${external_prereqs} "${p}") + endif() + else() + if(NOT "${p_type}" STREQUAL "local" AND NOT "${p_type}" STREQUAL "system") + set(external_prereqs ${external_prereqs} "${p}") + endif() + endif() + endforeach() + + if(external_prereqs) + # Found non-system/somehow-unacceptable prerequisites: + set(result 0) + set(info ${info} "external prerequisites found:\nf='${f}'\nexternal_prereqs='${external_prereqs}'\n") + endif() + endif() + endforeach() + + if(result) + set(info "Verified ${count} executable files in '${bundle}'") + endif() + + set(${result_var} "${result}" PARENT_SCOPE) + set(${info_var} "${info}" PARENT_SCOPE) +endfunction() + + +function(verify_bundle_symlinks bundle result_var info_var) + set(result 1) + set(info "") + set(count 0) + + # TODO: implement this function for real... + # Right now, it is just a stub that verifies unconditionally... + + set(${result_var} "${result}" PARENT_SCOPE) + set(${info_var} "${info}" PARENT_SCOPE) +endfunction() + + +function(verify_app app) + set(verified 0) + set(info "") + + get_bundle_and_executable("${app}" bundle executable valid) + + message(STATUS "===========================================================================") + message(STATUS "Analyzing app='${app}'") + message(STATUS "bundle='${bundle}'") + message(STATUS "executable='${executable}'") + message(STATUS "valid='${valid}'") + + # Verify that the bundle does not have any "external" prerequisites: + # + verify_bundle_prerequisites("${bundle}" verified info) + message(STATUS "verified='${verified}'") + message(STATUS "info='${info}'") + message(STATUS "") + + if(verified) + # Verify that the bundle does not have any symlinks to external files: + # + verify_bundle_symlinks("${bundle}" verified info) + message(STATUS "verified='${verified}'") + message(STATUS "info='${info}'") + message(STATUS "") + endif() + + if(NOT verified) + message(FATAL_ERROR "error: verify_app failed") + endif() +endfunction() diff --git a/ultimmc/cmake/GetGitRevisionDescription.cmake b/ultimmc/cmake/GetGitRevisionDescription.cmake new file mode 100644 index 0000000..39c2707 --- /dev/null +++ b/ultimmc/cmake/GetGitRevisionDescription.cmake @@ -0,0 +1,130 @@ +# - Returns a version string from Git +# +# These functions force a re-configure on each git commit so that you can +# trust the values of the variables in your build system. +# +# get_git_head_revision( [ ...]) +# +# Returns the refspec and sha hash of the current head revision +# +# git_describe( [ ...]) +# +# Returns the results of git describe on the source tree, and adjusting +# the output so that it tests false if an error occurs. +# +# git_get_exact_tag( [ ...]) +# +# Returns the results of git describe --exact-match on the source tree, +# and adjusting the output so that it tests false if there was no exact +# matching tag. +# +# Requires CMake 2.6 or newer (uses the 'function' command) +# +# Original Author: +# 2009-2010 Ryan Pavlik +# http://academic.cleardefinition.com +# Iowa State University HCI Graduate Program/VRAC +# +# Copyright Iowa State University 2009-2010. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) + +if(__get_git_revision_description) + return() +endif() +set(__get_git_revision_description YES) + +# We must run the following at "include" time, not at function call time, +# to find the path to this module rather than the path to a calling list file +get_filename_component(_gitdescmoddir ${CMAKE_CURRENT_LIST_FILE} PATH) + +function(get_git_head_revision _refspecvar _hashvar) + set(GIT_PARENT_DIR "${CMAKE_CURRENT_SOURCE_DIR}") + set(GIT_DIR "${GIT_PARENT_DIR}/.git") + while(NOT EXISTS "${GIT_DIR}") # .git dir not found, search parent directories + set(GIT_PREVIOUS_PARENT "${GIT_PARENT_DIR}") + get_filename_component(GIT_PARENT_DIR ${GIT_PARENT_DIR} PATH) + if(GIT_PARENT_DIR STREQUAL GIT_PREVIOUS_PARENT) + # We have reached the root directory, we are not in git + set(${_refspecvar} "GITDIR-NOTFOUND" PARENT_SCOPE) + set(${_hashvar} "GITDIR-NOTFOUND" PARENT_SCOPE) + return() + endif() + set(GIT_DIR "${GIT_PARENT_DIR}/.git") + endwhile() + # check if this is a submodule + if(NOT IS_DIRECTORY ${GIT_DIR}) + file(READ ${GIT_DIR} submodule) + string(REGEX REPLACE "gitdir: (.*)\n$" "\\1" GIT_DIR_RELATIVE ${submodule}) + get_filename_component(SUBMODULE_DIR ${GIT_DIR} PATH) + get_filename_component(GIT_DIR ${SUBMODULE_DIR}/${GIT_DIR_RELATIVE} ABSOLUTE) + endif() + set(GIT_DATA "${CMAKE_CURRENT_BINARY_DIR}/CMakeFiles/git-data") + if(NOT EXISTS "${GIT_DATA}") + file(MAKE_DIRECTORY "${GIT_DATA}") + endif() + + if(NOT EXISTS "${GIT_DIR}/HEAD") + return() + endif() + set(HEAD_FILE "${GIT_DATA}/HEAD") + configure_file("${GIT_DIR}/HEAD" "${HEAD_FILE}" COPYONLY) + + configure_file("${_gitdescmoddir}/GetGitRevisionDescription.cmake.in" + "${GIT_DATA}/grabRef.cmake" + @ONLY) + include("${GIT_DATA}/grabRef.cmake") + + set(${_refspecvar} "${HEAD_REF}" PARENT_SCOPE) + set(${_hashvar} "${HEAD_HASH}" PARENT_SCOPE) +endfunction() + +function(git_describe _var) + if(NOT GIT_FOUND) + find_package(Git QUIET) + endif() + get_git_head_revision(refspec hash) + if(NOT GIT_FOUND) + set(${_var} "GIT-NOTFOUND" PARENT_SCOPE) + return() + endif() + if(NOT hash) + set(${_var} "HEAD-HASH-NOTFOUND" PARENT_SCOPE) + return() + endif() + + # TODO sanitize + #if((${ARGN}" MATCHES "&&") OR + # (ARGN MATCHES "||") OR + # (ARGN MATCHES "\\;")) + # message("Please report the following error to the project!") + # message(FATAL_ERROR "Looks like someone's doing something nefarious with git_describe! Passed arguments ${ARGN}") + #endif() + + #message(STATUS "Arguments to execute_process: ${ARGN}") + + execute_process(COMMAND + "${GIT_EXECUTABLE}" + describe + ${hash} + ${ARGN} + WORKING_DIRECTORY + "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE + res + OUTPUT_VARIABLE + out + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT res EQUAL 0) + set(out "${out}-${res}-NOTFOUND") + endif() + + set(${_var} "${out}" PARENT_SCOPE) +endfunction() + +function(git_get_exact_tag _var) + git_describe(out --exact-match ${ARGN}) + set(${_var} "${out}" PARENT_SCOPE) +endfunction() diff --git a/ultimmc/cmake/GetGitRevisionDescription.cmake.in b/ultimmc/cmake/GetGitRevisionDescription.cmake.in new file mode 100644 index 0000000..04db9a8 --- /dev/null +++ b/ultimmc/cmake/GetGitRevisionDescription.cmake.in @@ -0,0 +1,41 @@ +# +# Internal file for GetGitRevisionDescription.cmake +# +# Requires CMake 2.6 or newer (uses the 'function' command) +# +# Original Author: +# 2009-2010 Ryan Pavlik +# http://academic.cleardefinition.com +# Iowa State University HCI Graduate Program/VRAC +# +# Copyright Iowa State University 2009-2010. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) + +set(HEAD_HASH) + +file(READ "@HEAD_FILE@" HEAD_CONTENTS LIMIT 1024) + +string(STRIP "${HEAD_CONTENTS}" HEAD_CONTENTS) +if(HEAD_CONTENTS MATCHES "ref") + # named branch + string(REPLACE "ref: " "" HEAD_REF "${HEAD_CONTENTS}") + if(EXISTS "@GIT_DIR@/${HEAD_REF}") + configure_file("@GIT_DIR@/${HEAD_REF}" "@GIT_DATA@/head-ref" COPYONLY) + else() + configure_file("@GIT_DIR@/packed-refs" "@GIT_DATA@/packed-refs" COPYONLY) + file(READ "@GIT_DATA@/packed-refs" PACKED_REFS) + if(${PACKED_REFS} MATCHES "([0-9a-z]*) ${HEAD_REF}") + set(HEAD_HASH "${CMAKE_MATCH_1}") + endif() + endif() +else() + # detached HEAD + configure_file("@GIT_DIR@/HEAD" "@GIT_DATA@/head-ref" COPYONLY) +endif() + +if(NOT HEAD_HASH) + file(READ "@GIT_DATA@/head-ref" HEAD_HASH LIMIT 1024) + string(STRIP "${HEAD_HASH}" HEAD_HASH) +endif() diff --git a/ultimmc/cmake/GetPrerequisites.cmake b/ultimmc/cmake/GetPrerequisites.cmake new file mode 100644 index 0000000..39c2cc6 --- /dev/null +++ b/ultimmc/cmake/GetPrerequisites.cmake @@ -0,0 +1,902 @@ +# - Functions to analyze and list executable file prerequisites. +# This module provides functions to list the .dll, .dylib or .so +# files that an executable or shared library file depends on. (Its +# prerequisites.) +# +# It uses various tools to obtain the list of required shared library files: +# dumpbin (Windows) +# objdump (MinGW on Windows) +# ldd (Linux/Unix) +# otool (Mac OSX) +# The following functions are provided by this module: +# get_prerequisites +# list_prerequisites +# list_prerequisites_by_glob +# gp_append_unique +# is_file_executable +# gp_item_default_embedded_path +# (projects can override with gp_item_default_embedded_path_override) +# gp_resolve_item +# (projects can override with gp_resolve_item_override) +# gp_resolved_file_type +# (projects can override with gp_resolved_file_type_override) +# gp_file_type +# Requires CMake 2.6 or greater because it uses function, break, return and +# PARENT_SCOPE. +# +# GET_PREREQUISITES( +# ) +# Get the list of shared library files required by . The list in +# the variable named should be empty on first entry to +# this function. On exit, will contain the list of +# required shared library files. +# +# is the full path to an executable file. is the +# name of a CMake variable to contain the results. must be 0 +# or 1 indicating whether to include or exclude "system" prerequisites. If +# is set to 1 all prerequisites will be found recursively, if set to +# 0 only direct prerequisites are listed. is the path to the top +# level executable used for @executable_path replacment on the Mac. is +# a list of paths where libraries might be found: these paths are searched +# first when a target without any path info is given. Then standard system +# locations are also searched: PATH, Framework locations, /usr/lib... +# +# LIST_PREREQUISITES( [ [ []]]) +# Print a message listing the prerequisites of . +# +# is the name of a shared library or executable target or the full +# path to a shared library or executable file. If is set to 1 all +# prerequisites will be found recursively, if set to 0 only direct +# prerequisites are listed. must be 0 or 1 indicating whether +# to include or exclude "system" prerequisites. With set to 0 only +# the full path names of the prerequisites are printed, set to 1 extra +# informatin will be displayed. +# +# LIST_PREREQUISITES_BY_GLOB( ) +# Print the prerequisites of shared library and executable files matching a +# globbing pattern. is GLOB or GLOB_RECURSE and is a +# globbing expression used with "file(GLOB" or "file(GLOB_RECURSE" to retrieve +# a list of matching files. If a matching file is executable, its prerequisites +# are listed. +# +# Any additional (optional) arguments provided are passed along as the +# optional arguments to the list_prerequisites calls. +# +# GP_APPEND_UNIQUE( ) +# Append to the list variable only if the value is not +# already in the list. +# +# IS_FILE_EXECUTABLE( ) +# Return 1 in if is a binary executable, 0 otherwise. +# +# GP_ITEM_DEFAULT_EMBEDDED_PATH( ) +# Return the path that others should refer to the item by when the item +# is embedded inside a bundle. +# +# Override on a per-project basis by providing a project-specific +# gp_item_default_embedded_path_override function. +# +# GP_RESOLVE_ITEM( ) +# Resolve an item into an existing full path file. +# +# Override on a per-project basis by providing a project-specific +# gp_resolve_item_override function. +# +# GP_RESOLVED_FILE_TYPE( ) +# Return the type of with respect to . String +# describing type of prerequisite is returned in variable named . +# +# Use and if necessary to resolve non-absolute +# values -- but only for non-embedded items. +# +# Possible types are: +# system +# local +# embedded +# other +# Override on a per-project basis by providing a project-specific +# gp_resolved_file_type_override function. +# +# GP_FILE_TYPE( ) +# Return the type of with respect to . String +# describing type of prerequisite is returned in variable named . +# +# Possible types are: +# system +# local +# embedded +# other + +#============================================================================= +# Copyright 2008-2009 Kitware, Inc. +# +# Distributed under the OSI-approved BSD License (the "License"); +# see accompanying file Copyright.txt for details. +# +# This software is distributed WITHOUT ANY WARRANTY; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the License for more information. +#============================================================================= +# (To distribute this file outside of CMake, substitute the full +# License text for the above reference.) + +function(gp_append_unique list_var value) + set(contains 0) + + foreach(item ${${list_var}}) + if("${item}" STREQUAL "${value}") + set(contains 1) + break() + endif() + endforeach() + + if(NOT contains) + set(${list_var} ${${list_var}} "${value}" PARENT_SCOPE) + endif() +endfunction() + + +function(is_file_executable file result_var) + # + # A file is not executable until proven otherwise: + # + set(${result_var} 0 PARENT_SCOPE) + + get_filename_component(file_full "${file}" ABSOLUTE) + string(TOLOWER "${file_full}" file_full_lower) + + # If file name ends in .exe on Windows, *assume* executable: + # + if(WIN32 AND NOT UNIX) + if("${file_full_lower}" MATCHES "\\.exe$") + set(${result_var} 1 PARENT_SCOPE) + return() + endif() + + # A clause could be added here that uses output or return value of dumpbin + # to determine ${result_var}. In 99%+? practical cases, the exe name + # match will be sufficient... + # + endif() + + # Use the information returned from the Unix shell command "file" to + # determine if ${file_full} should be considered an executable file... + # + # If the file command's output contains "executable" and does *not* contain + # "text" then it is likely an executable suitable for prerequisite analysis + # via the get_prerequisites macro. + # + if(UNIX) + if(NOT file_cmd) + find_program(file_cmd "file") + mark_as_advanced(file_cmd) + endif() + + if(file_cmd) + execute_process(COMMAND "${file_cmd}" "${file_full}" + OUTPUT_VARIABLE file_ov + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + # Replace the name of the file in the output with a placeholder token + # (the string " _file_full_ ") so that just in case the path name of + # the file contains the word "text" or "executable" we are not fooled + # into thinking "the wrong thing" because the file name matches the + # other 'file' command output we are looking for... + # + string(REPLACE "${file_full}" " _file_full_ " file_ov "${file_ov}") + string(TOLOWER "${file_ov}" file_ov) + + #message(STATUS "file_ov='${file_ov}'") + if("${file_ov}" MATCHES "executable") + #message(STATUS "executable!") + if("${file_ov}" MATCHES "text") + #message(STATUS "but text, so *not* a binary executable!") + else() + set(${result_var} 1 PARENT_SCOPE) + return() + endif() + endif() + + # Also detect position independent executables on Linux, + # where "file" gives "shared object ... (uses shared libraries)" + if("${file_ov}" MATCHES "shared object.*\(uses shared libs\)") + set(${result_var} 1 PARENT_SCOPE) + return() + endif() + + # "file" version 5.22 does not print "(used shared libraries)" + # but uses "interpreter" + if("${file_ov}" MATCHES "shared object.*interpreter") + set(${result_var} 1 PARENT_SCOPE) + return() + endif() + + else() + message(STATUS "warning: No 'file' command, skipping execute_process...") + endif() + endif() +endfunction() + + +function(gp_item_default_embedded_path item default_embedded_path_var) + + # On Windows and Linux, "embed" prerequisites in the same directory + # as the executable by default: + # + set(path "@executable_path") + set(overridden 0) + + # On the Mac, relative to the executable depending on the type + # of the thing we are embedding: + # + if(APPLE) + # + # The assumption here is that all executables in the bundle will be + # in same-level-directories inside the bundle. The parent directory + # of an executable inside the bundle should be MacOS or a sibling of + # MacOS and all embedded paths returned from here will begin with + # "@executable_path/../" and will work from all executables in all + # such same-level-directories inside the bundle. + # + + # By default, embed things right next to the main bundle executable: + # + set(path "@executable_path/../../Contents/MacOS") + + # Embed .dylibs right next to the main bundle executable: + # + if(item MATCHES "\\.dylib$") + set(path "@executable_path/../MacOS") + set(overridden 1) + endif() + + # Embed frameworks in the embedded "Frameworks" directory (sibling of MacOS): + # + if(NOT overridden) + if(item MATCHES "[^/]+\\.framework/") + set(path "@executable_path/../Frameworks") + set(overridden 1) + endif() + endif() + endif() + + # Provide a hook so that projects can override the default embedded location + # of any given library by whatever logic they choose: + # + if(COMMAND gp_item_default_embedded_path_override) + gp_item_default_embedded_path_override("${item}" path) + endif() + + set(${default_embedded_path_var} "${path}" PARENT_SCOPE) +endfunction() + + +function(gp_resolve_item context item exepath dirs resolved_item_var) + set(resolved 0) + set(resolved_item "${item}") + + # Is it already resolved? + # + if(IS_ABSOLUTE "${resolved_item}" AND EXISTS "${resolved_item}") + set(resolved 1) + endif() + + if(NOT resolved) + if(item MATCHES "@executable_path") + # + # @executable_path references are assumed relative to exepath + # + string(REPLACE "@executable_path" "${exepath}" ri "${item}") + get_filename_component(ri "${ri}" ABSOLUTE) + + if(EXISTS "${ri}") + #message(STATUS "info: embedded item exists (${ri})") + set(resolved 1) + set(resolved_item "${ri}") + else() + message(STATUS "warning: embedded item does not exist '${ri}'") + endif() + endif() + endif() + + if(NOT resolved) + if(item MATCHES "@loader_path") + # + # @loader_path references are assumed relative to the + # PATH of the given "context" (presumably another library) + # + get_filename_component(contextpath "${context}" PATH) + string(REPLACE "@loader_path" "${contextpath}" ri "${item}") + get_filename_component(ri "${ri}" ABSOLUTE) + + if(EXISTS "${ri}") + #message(STATUS "info: embedded item exists (${ri})") + set(resolved 1) + set(resolved_item "${ri}") + else() + message(STATUS "warning: embedded item does not exist '${ri}'") + endif() + endif() + endif() + + if(NOT resolved) + if(item MATCHES "@rpath") + # + # @rpath references are relative to the paths built into the binaries with -rpath + # We handle this case like we do for other Unixes + # + string(REPLACE "@rpath/" "" norpath_item "${item}") + + set(ri "ri-NOTFOUND") + find_file(ri "${norpath_item}" ${exepath} ${dirs} NO_DEFAULT_PATH) + if(ri) + #message(STATUS "info: 'find_file' in exepath/dirs (${ri})") + set(resolved 1) + set(resolved_item "${ri}") + set(ri "ri-NOTFOUND") + endif() + + endif() + endif() + + if(NOT resolved) + set(ri "ri-NOTFOUND") + find_file(ri "${item}" ${exepath} ${dirs} NO_DEFAULT_PATH) + find_file(ri "${item}" ${exepath} ${dirs} /usr/lib) + if(ri) + #message(STATUS "info: 'find_file' in exepath/dirs (${ri})") + set(resolved 1) + set(resolved_item "${ri}") + set(ri "ri-NOTFOUND") + endif() + endif() + + if(NOT resolved) + if(item MATCHES "[^/]+\\.framework/") + set(fw "fw-NOTFOUND") + find_file(fw "${item}" + "~/Library/Frameworks" + "/Library/Frameworks" + "/System/Library/Frameworks" + ) + if(fw) + #message(STATUS "info: 'find_file' found framework (${fw})") + set(resolved 1) + set(resolved_item "${fw}") + set(fw "fw-NOTFOUND") + endif() + endif() + endif() + + # Using find_program on Windows will find dll files that are in the PATH. + # (Converting simple file names into full path names if found.) + # + if(WIN32 AND NOT UNIX) + if(NOT resolved) + set(ri "ri-NOTFOUND") + find_program(ri "${item}" PATHS "${exepath};${dirs}" NO_DEFAULT_PATH) + find_program(ri "${item}" PATHS "${exepath};${dirs}") + if(ri) + #message(STATUS "info: 'find_program' in exepath/dirs (${ri})") + set(resolved 1) + set(resolved_item "${ri}") + set(ri "ri-NOTFOUND") + endif() + endif() + endif() + + # Provide a hook so that projects can override item resolution + # by whatever logic they choose: + # + if(COMMAND gp_resolve_item_override) + gp_resolve_item_override("${context}" "${item}" "${exepath}" "${dirs}" resolved_item resolved) + endif() + + if(NOT resolved) + message(STATUS " +warning: cannot resolve item '${item}' + + possible problems: + need more directories? + need to use InstallRequiredSystemLibraries? + run in install tree instead of build tree? +") +# message(STATUS " +#****************************************************************************** +#warning: cannot resolve item '${item}' +# +# possible problems: +# need more directories? +# need to use InstallRequiredSystemLibraries? +# run in install tree instead of build tree? +# +# context='${context}' +# item='${item}' +# exepath='${exepath}' +# dirs='${dirs}' +# resolved_item_var='${resolved_item_var}' +#****************************************************************************** +#") + endif() + + set(${resolved_item_var} "${resolved_item}" PARENT_SCOPE) +endfunction() + + +function(gp_resolved_file_type original_file file exepath dirs type_var) + #message(STATUS "**") + + if(NOT IS_ABSOLUTE "${original_file}") + message(STATUS "warning: gp_resolved_file_type expects absolute full path for first arg original_file") + endif() + + set(is_embedded 0) + set(is_local 0) + set(is_system 0) + + set(resolved_file "${file}") + + if("${file}" MATCHES "^@(executable|loader)_path") + set(is_embedded 1) + endif() + + if(NOT is_embedded) + if(NOT IS_ABSOLUTE "${file}") + gp_resolve_item("${original_file}" "${file}" "${exepath}" "${dirs}" resolved_file) + endif() + + string(TOLOWER "${original_file}" original_lower) + string(TOLOWER "${resolved_file}" lower) + + if(UNIX) + if(resolved_file MATCHES "^(/lib/|/lib32/|/lib64/|/usr/lib/|/usr/lib32/|/usr/lib64/|/usr/X11R6/|/usr/bin/)") + set(is_system 1) + endif() + endif() + + if(APPLE) + if(resolved_file MATCHES "^(/System/Library/|/usr/lib/)") + set(is_system 1) + endif() + endif() + + if(WIN32) + string(TOLOWER "$ENV{SystemRoot}" sysroot) + string(REGEX REPLACE "\\\\" "/" sysroot "${sysroot}") + + string(TOLOWER "$ENV{windir}" windir) + string(REGEX REPLACE "\\\\" "/" windir "${windir}") + + if(lower MATCHES "^(${sysroot}/sys(tem|wow)|${windir}/sys(tem|wow)|(.*/)*msvc[^/]+dll)") + set(is_system 1) + endif() + + if(UNIX) + # if cygwin, we can get the properly formed windows paths from cygpath + find_program(CYGPATH_EXECUTABLE cygpath) + + if(CYGPATH_EXECUTABLE) + execute_process(COMMAND ${CYGPATH_EXECUTABLE} -W + OUTPUT_VARIABLE env_windir + OUTPUT_STRIP_TRAILING_WHITESPACE) + execute_process(COMMAND ${CYGPATH_EXECUTABLE} -S + OUTPUT_VARIABLE env_sysdir + OUTPUT_STRIP_TRAILING_WHITESPACE) + string(TOLOWER "${env_windir}" windir) + string(TOLOWER "${env_sysdir}" sysroot) + + if(lower MATCHES "^(${sysroot}/sys(tem|wow)|${windir}/sys(tem|wow)|(.*/)*msvc[^/]+dll)") + set(is_system 1) + endif() + endif() + endif() + endif() + + if(NOT is_system) + get_filename_component(original_path "${original_lower}" PATH) + get_filename_component(path "${lower}" PATH) + if("${original_path}" STREQUAL "${path}") + set(is_local 1) + else() + string(LENGTH "${original_path}/" original_length) + string(LENGTH "${lower}" path_length) + if(${path_length} GREATER ${original_length}) + string(SUBSTRING "${lower}" 0 ${original_length} path) + if("${original_path}/" STREQUAL "${path}") + set(is_embedded 1) + endif() + endif() + endif() + endif() + endif() + + # Return type string based on computed booleans: + # + set(type "other") + + if(is_system) + set(type "system") + elseif(is_embedded) + set(type "embedded") + elseif(is_local) + set(type "local") + endif() + + #message(STATUS "gp_resolved_file_type: '${file}' '${resolved_file}'") + #message(STATUS " type: '${type}'") + + if(NOT is_embedded) + if(NOT IS_ABSOLUTE "${resolved_file}") + if(lower MATCHES "^msvc[^/]+dll" AND is_system) + message(STATUS "info: non-absolute msvc file '${file}' returning type '${type}'") + else() + message(STATUS "warning: gp_resolved_file_type non-absolute file '${file}' returning type '${type}' -- possibly incorrect") + endif() + endif() + endif() + + # Provide a hook so that projects can override the decision on whether a + # library belongs to the system or not by whatever logic they choose: + # + if(COMMAND gp_resolved_file_type_override) + gp_resolved_file_type_override("${resolved_file}" type) + endif() + + set(${type_var} "${type}" PARENT_SCOPE) + + #message(STATUS "**") +endfunction() + + +function(gp_file_type original_file file type_var) + if(NOT IS_ABSOLUTE "${original_file}") + message(STATUS "warning: gp_file_type expects absolute full path for first arg original_file") + endif() + + get_filename_component(exepath "${original_file}" PATH) + + set(type "") + gp_resolved_file_type("${original_file}" "${file}" "${exepath}" "" type) + + set(${type_var} "${type}" PARENT_SCOPE) +endfunction() + + +function(get_prerequisites target prerequisites_var exclude_system recurse exepath dirs) + set(verbose 0) + set(eol_char "E") + + if(NOT IS_ABSOLUTE "${target}") + message("warning: target '${target}' is not absolute...") + endif() + + if(NOT EXISTS "${target}") + message("warning: target '${target}' does not exist...") + endif() + + set(gp_cmd_paths ${gp_cmd_paths} + "C:/Program Files/Microsoft Visual Studio 9.0/VC/bin" + "C:/Program Files (x86)/Microsoft Visual Studio 9.0/VC/bin" + "C:/Program Files/Microsoft Visual Studio 8/VC/BIN" + "C:/Program Files (x86)/Microsoft Visual Studio 8/VC/BIN" + "C:/Program Files/Microsoft Visual Studio .NET 2003/VC7/BIN" + "C:/Program Files (x86)/Microsoft Visual Studio .NET 2003/VC7/BIN" + "/usr/local/bin" + "/usr/bin" + ) + + # + # + # Try to choose the right tool by default. Caller can set gp_tool prior to + # calling this function to force using a different tool. + # + if("${gp_tool}" STREQUAL "") + set(gp_tool "ldd") + + if(APPLE) + set(gp_tool "otool") + endif() + + if(WIN32 AND NOT UNIX) # This is how to check for cygwin, har! + find_program(gp_dumpbin "dumpbin" PATHS ${gp_cmd_paths}) + if(gp_dumpbin) + set(gp_tool "dumpbin") + else() # Try harder. Maybe we're on MinGW + set(gp_tool "objdump") + endif() + endif() + endif() + + find_program(gp_cmd ${gp_tool} PATHS ${gp_cmd_paths}) + + if(NOT gp_cmd) + message(FATAL_ERROR "FATAL ERROR: could not find '${gp_tool}' - cannot analyze prerequisites!") + return() + endif() + + set(gp_tool_known 0) + + if("${gp_tool}" STREQUAL "ldd") + set(gp_cmd_args "") + set(gp_regex "^[\t ]*[^\t ]+ => ([^\t\(]+) .*${eol_char}$") + set(gp_regex_error "not found${eol_char}$") + set(gp_regex_fallback "^[\t ]*([^\t ]+) => ([^\t ]+).*${eol_char}$") + set(gp_regex_cmp_count 1) + set(gp_tool_known 1) + endif() + + if("${gp_tool}" STREQUAL "otool") + set(gp_cmd_args "-L") + set(gp_regex "^\t([^\t]+) \\(compatibility version ([0-9]+.[0-9]+.[0-9]+), current version ([0-9]+.[0-9]+.[0-9]+)\\)${eol_char}$") + set(gp_regex_error "") + set(gp_regex_fallback "") + set(gp_regex_cmp_count 3) + set(gp_tool_known 1) + endif() + + if("${gp_tool}" STREQUAL "dumpbin") + set(gp_cmd_args "/dependents") + set(gp_regex "^ ([^ ].*[Dd][Ll][Ll])${eol_char}$") + set(gp_regex_error "") + set(gp_regex_fallback "") + set(gp_regex_cmp_count 1) + set(gp_tool_known 1) + endif() + + if("${gp_tool}" STREQUAL "objdump") + set(gp_cmd_args "-p") + set(gp_regex "^\t*DLL Name: (.*\\.[Dd][Ll][Ll])${eol_char}$") + set(gp_regex_error "") + set(gp_regex_fallback "") + set(gp_regex_cmp_count 1) + set(gp_tool_known 1) + endif() + + if(NOT gp_tool_known) + message(STATUS "warning: gp_tool='${gp_tool}' is an unknown tool...") + message(STATUS "CMake function get_prerequisites needs more code to handle '${gp_tool}'") + message(STATUS "Valid gp_tool values are dumpbin, ldd, objdump and otool.") + return() + endif() + + + if("${gp_tool}" STREQUAL "dumpbin") + # When running dumpbin, it also needs the "Common7/IDE" directory in the + # PATH. It will already be in the PATH if being run from a Visual Studio + # command prompt. Add it to the PATH here in case we are running from a + # different command prompt. + # + get_filename_component(gp_cmd_dir "${gp_cmd}" PATH) + get_filename_component(gp_cmd_dlls_dir "${gp_cmd_dir}/../../Common7/IDE" ABSOLUTE) + # Use cmake paths as a user may have a PATH element ending with a backslash. + # This will escape the list delimiter and create havoc! + if(EXISTS "${gp_cmd_dlls_dir}") + # only add to the path if it is not already in the path + set(gp_found_cmd_dlls_dir 0) + file(TO_CMAKE_PATH "$ENV{PATH}" env_path) + foreach(gp_env_path_element ${env_path}) + if("${gp_env_path_element}" STREQUAL "${gp_cmd_dlls_dir}") + set(gp_found_cmd_dlls_dir 1) + endif() + endforeach() + + if(NOT gp_found_cmd_dlls_dir) + file(TO_NATIVE_PATH "${gp_cmd_dlls_dir}" gp_cmd_dlls_dir) + set(ENV{PATH} "$ENV{PATH};${gp_cmd_dlls_dir}") + endif() + endif() + endif() + # + # + + if("${gp_tool}" STREQUAL "ldd") + set(old_ld_env "$ENV{LD_LIBRARY_PATH}") + foreach(dir ${exepath} ${dirs}) + set(ENV{LD_LIBRARY_PATH} "${dir}:$ENV{LD_LIBRARY_PATH}") + endforeach() + endif() + + + # Track new prerequisites at each new level of recursion. Start with an + # empty list at each level: + # + set(unseen_prereqs) + + # Run gp_cmd on the target: + # + execute_process( + COMMAND ${gp_cmd} ${gp_cmd_args} ${target} + OUTPUT_VARIABLE gp_cmd_ov + ) + + if("${gp_tool}" STREQUAL "ldd") + set(ENV{LD_LIBRARY_PATH} "${old_ld_env}") + endif() + + if(verbose) + message(STATUS "") + message(STATUS "gp_cmd_ov='${gp_cmd_ov}'") + message(STATUS "") + endif() + + get_filename_component(target_dir "${target}" PATH) + + # Convert to a list of lines: + # + string(REGEX REPLACE ";" "\\\\;" candidates "${gp_cmd_ov}") + string(REGEX REPLACE "\n" "${eol_char};" candidates "${candidates}") + + # check for install id and remove it from list, since otool -L can include a + # reference to itself + set(gp_install_id) + if("${gp_tool}" STREQUAL "otool") + execute_process( + COMMAND otool -D ${target} + OUTPUT_VARIABLE gp_install_id_ov + ) + # second line is install name + string(REGEX REPLACE ".*:\n" "" gp_install_id "${gp_install_id_ov}") + if(gp_install_id) + # trim + string(REGEX MATCH "[^\n ].*[^\n ]" gp_install_id "${gp_install_id}") + #message("INSTALL ID is \"${gp_install_id}\"") + endif() + endif() + + # Analyze each line for file names that match the regular expression: + # + foreach(candidate ${candidates}) + if("${candidate}" MATCHES "${gp_regex}") + + # Extract information from each candidate: + if(gp_regex_error AND "${candidate}" MATCHES "${gp_regex_error}") + string(REGEX REPLACE "${gp_regex_fallback}" "\\1" raw_item "${candidate}") + else() + string(REGEX REPLACE "${gp_regex}" "\\1" raw_item "${candidate}") + endif() + + if(gp_regex_cmp_count GREATER 1) + string(REGEX REPLACE "${gp_regex}" "\\2" raw_compat_version "${candidate}") + string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\1" compat_major_version "${raw_compat_version}") + string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\2" compat_minor_version "${raw_compat_version}") + string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\3" compat_patch_version "${raw_compat_version}") + endif() + + if(gp_regex_cmp_count GREATER 2) + string(REGEX REPLACE "${gp_regex}" "\\3" raw_current_version "${candidate}") + string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\1" current_major_version "${raw_current_version}") + string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\2" current_minor_version "${raw_current_version}") + string(REGEX REPLACE "^([0-9]+)\\.([0-9]+)\\.([0-9]+)$" "\\3" current_patch_version "${raw_current_version}") + endif() + + # Use the raw_item as the list entries returned by this function. Use the + # gp_resolve_item function to resolve it to an actual full path file if + # necessary. + # + set(item "${raw_item}") + + # Add each item unless it is excluded: + # + set(add_item 1) + + if("${item}" STREQUAL "${gp_install_id}") + set(add_item 0) + endif() + + if(add_item AND ${exclude_system}) + set(type "") + gp_resolved_file_type("${target}" "${item}" "${exepath}" "${dirs}" type) + + if("${type}" STREQUAL "system") + set(add_item 0) + endif() + endif() + + if(add_item) + list(LENGTH ${prerequisites_var} list_length_before_append) + gp_append_unique(${prerequisites_var} "${item}") + list(LENGTH ${prerequisites_var} list_length_after_append) + + if(${recurse}) + # If item was really added, this is the first time we have seen it. + # Add it to unseen_prereqs so that we can recursively add *its* + # prerequisites... + # + # But first: resolve its name to an absolute full path name such + # that the analysis tools can simply accept it as input. + # + if(NOT list_length_before_append EQUAL list_length_after_append) + gp_resolve_item("${target}" "${item}" "${exepath}" "${dirs}" resolved_item) + set(unseen_prereqs ${unseen_prereqs} "${resolved_item}") + endif() + endif() + endif() + else() + if(verbose) + message(STATUS "ignoring non-matching line: '${candidate}'") + endif() + endif() + endforeach() + + list(LENGTH ${prerequisites_var} prerequisites_var_length) + if(prerequisites_var_length GREATER 0) + list(SORT ${prerequisites_var}) + endif() + if(${recurse}) + set(more_inputs ${unseen_prereqs}) + foreach(input ${more_inputs}) + get_prerequisites("${input}" ${prerequisites_var} ${exclude_system} ${recurse} "${exepath}" "${dirs}") + endforeach() + endif() + + set(${prerequisites_var} ${${prerequisites_var}} PARENT_SCOPE) +endfunction() + + +function(list_prerequisites target) + if("${ARGV1}" STREQUAL "") + set(all 1) + else() + set(all "${ARGV1}") + endif() + + if("${ARGV2}" STREQUAL "") + set(exclude_system 0) + else() + set(exclude_system "${ARGV2}") + endif() + + if("${ARGV3}" STREQUAL "") + set(verbose 0) + else() + set(verbose "${ARGV3}") + endif() + + set(count 0) + set(count_str "") + set(print_count "${verbose}") + set(print_prerequisite_type "${verbose}") + set(print_target "${verbose}") + set(type_str "") + + get_filename_component(exepath "${target}" PATH) + + set(prereqs "") + get_prerequisites("${target}" prereqs ${exclude_system} ${all} "${exepath}" "") + + if(print_target) + message(STATUS "File '${target}' depends on:") + endif() + + foreach(d ${prereqs}) + math(EXPR count "${count} + 1") + + if(print_count) + set(count_str "${count}. ") + endif() + + if(print_prerequisite_type) + gp_file_type("${target}" "${d}" type) + set(type_str " (${type})") + endif() + + message(STATUS "${count_str}${d}${type_str}") + endforeach() +endfunction() + + +function(list_prerequisites_by_glob glob_arg glob_exp) + message(STATUS "=============================================================================") + message(STATUS "List prerequisites of executables matching ${glob_arg} '${glob_exp}'") + message(STATUS "") + file(${glob_arg} file_list ${glob_exp}) + foreach(f ${file_list}) + is_file_executable("${f}" is_f_executable) + if(is_f_executable) + message(STATUS "=============================================================================") + list_prerequisites("${f}" ${ARGN}) + message(STATUS "") + endif() + endforeach() +endfunction() diff --git a/ultimmc/cmake/GitFunctions.cmake b/ultimmc/cmake/GitFunctions.cmake new file mode 100644 index 0000000..a055b5d --- /dev/null +++ b/ultimmc/cmake/GitFunctions.cmake @@ -0,0 +1,37 @@ +if(__GITFUNCTIONS_CMAKE__) + return() +endif() +set(__GITFUNCTIONS_CMAKE__ TRUE) + +find_package(Git QUIET) + +include(CMakeParseArguments) + +if(GIT_FOUND) + function(git_run) + set(oneValueArgs OUTPUT_VAR DEFAULT) + set(multiValueArgs COMMAND) + cmake_parse_arguments(GIT_RUN "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + + execute_process(COMMAND ${GIT_EXECUTABLE} ${GIT_RUN_COMMAND} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + RESULT_VARIABLE GIT_RESULTVAR + OUTPUT_VARIABLE GIT_OUTVAR + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + + if(GIT_RESULTVAR EQUAL 0) + set(${GIT_RUN_OUTPUT_VAR} "${GIT_OUTVAR}" PARENT_SCOPE) + else() + set(${GIT_RUN_OUTPUT_VAR} ${GIT_RUN_DEFAULT}) + message(STATUS "Failed to run Git: ${GIT_OUTVAR}") + endif() + endfunction() +else() + function(git_run) + set(oneValueArgs OUTPUT_VAR DEFAULT) + set(multiValueArgs COMMAND) + cmake_parse_arguments(GIT_RUN "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) + set(${GIT_RUN_OUTPUT_VAR} ${GIT_RUN_DEFAULT}) + endfunction(git_run) +endif() diff --git a/ultimmc/cmake/MacOSXBundleInfo.plist.in b/ultimmc/cmake/MacOSXBundleInfo.plist.in new file mode 100644 index 0000000..3302244 --- /dev/null +++ b/ultimmc/cmake/MacOSXBundleInfo.plist.in @@ -0,0 +1,42 @@ + + + + + NSPrincipalClass + NSApplication + NSHighResolutionCapable + True + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature + ???? + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + LSRequiresCarbon + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + NSMicrophoneUsageDescription + MultiMC does not need access to your microphone for itself. Likely it's requesting it on behalf of a Minecraft mod you installed, which needs to access your microphone in order to function properly. + + diff --git a/ultimmc/cmake/QMakeQuery.cmake b/ultimmc/cmake/QMakeQuery.cmake new file mode 100644 index 0000000..bf0fe96 --- /dev/null +++ b/ultimmc/cmake/QMakeQuery.cmake @@ -0,0 +1,14 @@ +if(__QMAKEQUERY_CMAKE__) + return() +endif() +set(__QMAKEQUERY_CMAKE__ TRUE) + +get_target_property(QMAKE_EXECUTABLE Qt5::qmake LOCATION) + +function(QUERY_QMAKE VAR RESULT) + exec_program(${QMAKE_EXECUTABLE} ARGS "-query ${VAR}" RETURN_VALUE return_code OUTPUT_VARIABLE output ) + if(NOT return_code) + file(TO_CMAKE_PATH "${output}" output) + set(${RESULT} ${output} PARENT_SCOPE) + endif(NOT return_code) +endfunction(QUERY_QMAKE) diff --git a/ultimmc/cmake/UnitTest.cmake b/ultimmc/cmake/UnitTest.cmake new file mode 100644 index 0000000..9f2bc26 --- /dev/null +++ b/ultimmc/cmake/UnitTest.cmake @@ -0,0 +1,48 @@ +find_package(Qt5Test REQUIRED) + +set(TEST_RESOURCE_PATH ${CMAKE_CURRENT_LIST_DIR}) + +message(${TEST_RESOURCE_PATH}) + +function(add_unit_test name) + set(options "") + set(oneValueArgs DATA) + set(multiValueArgs SOURCES LIBS) + + cmake_parse_arguments(OPT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} ) + + if(WIN32) + add_executable(${name}_test ${OPT_SOURCES} ${TEST_RESOURCE_PATH}/UnitTest/test.rc) + else() + add_executable(${name}_test ${OPT_SOURCES}) + endif() + + if(NOT "${OPT_DATA}" STREQUAL "") + set(TEST_DATA_PATH "${CMAKE_CURRENT_BINARY_DIR}/data") + set(TEST_DATA_PATH_SRC "${CMAKE_CURRENT_SOURCE_DIR}/${OPT_DATA}") + message("From ${TEST_DATA_PATH_SRC} to ${TEST_DATA_PATH}") + string(REGEX REPLACE "[/\\:]" "_" DATA_TARGET_NAME "${TEST_DATA_PATH_SRC}") + if(UNIX) + # on unix we get the third / from the filename + set(TEST_DATA_URL "file://${TEST_DATA_PATH}") + else() + # we don't on windows, so we have to add it ourselves + set(TEST_DATA_URL "file:///${TEST_DATA_PATH}") + endif() + if(NOT TARGET "${DATA_TARGET_NAME}") + add_custom_target(${DATA_TARGET_NAME}) + add_dependencies(${name}_test ${DATA_TARGET_NAME}) + add_custom_command( + TARGET ${DATA_TARGET_NAME} + COMMAND ${CMAKE_COMMAND} "-DTEST_DATA_URL=${TEST_DATA_URL}" -DSOURCE=${TEST_DATA_PATH_SRC} -DDESTINATION=${TEST_DATA_PATH} -P ${TEST_RESOURCE_PATH}/UnitTest/generate_test_data.cmake + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif() + endif() + + target_link_libraries(${name}_test Qt5::Test ${OPT_LIBS}) + + target_include_directories(${name}_test PRIVATE "${TEST_RESOURCE_PATH}/UnitTest/") + + add_test(NAME ${name} COMMAND ${name}_test WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) +endfunction() diff --git a/ultimmc/cmake/UnitTest/TestUtil.h b/ultimmc/cmake/UnitTest/TestUtil.h new file mode 100644 index 0000000..ebe3c66 --- /dev/null +++ b/ultimmc/cmake/UnitTest/TestUtil.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include + +#define expandstr(s) expandstr2(s) +#define expandstr2(s) #s + +class TestsInternal +{ +public: + static QByteArray readFile(const QString &fileName) + { + QFile f(fileName); + f.open(QFile::ReadOnly); + return f.readAll(); + } + static QString readFileUtf8(const QString &fileName) + { + return QString::fromUtf8(readFile(fileName)); + } +}; + +#define GET_TEST_FILE(file) TestsInternal::readFile(QFINDTESTDATA(file)) +#define GET_TEST_FILE_UTF8(file) TestsInternal::readFileUtf8(QFINDTESTDATA(file)) + diff --git a/ultimmc/cmake/UnitTest/generate_test_data.cmake b/ultimmc/cmake/UnitTest/generate_test_data.cmake new file mode 100644 index 0000000..d0bd4ab --- /dev/null +++ b/ultimmc/cmake/UnitTest/generate_test_data.cmake @@ -0,0 +1,23 @@ +# Copy files from source directory to destination directory, substituting any +# variables. Create destination directory if it does not exist. + +function(configure_files srcDir destDir) + make_directory(${destDir}) + + file(GLOB templateFiles RELATIVE ${srcDir} ${srcDir}/*) + foreach(templateFile ${templateFiles}) + set(srcTemplatePath ${srcDir}/${templateFile}) + if(NOT IS_DIRECTORY ${srcTemplatePath}) + configure_file( + ${srcTemplatePath} + ${destDir}/${templateFile} + @ONLY + NEWLINE_STYLE LF + ) + else() + configure_files("${srcTemplatePath}" "${destDir}/${templateFile}") + endif() + endforeach() +endfunction() + +configure_files(${SOURCE} ${DESTINATION}) \ No newline at end of file diff --git a/ultimmc/cmake/UnitTest/test.manifest b/ultimmc/cmake/UnitTest/test.manifest new file mode 100644 index 0000000..dc5f9d8 --- /dev/null +++ b/ultimmc/cmake/UnitTest/test.manifest @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + Custom Minecraft launcher for managing multiple installs. + + + + + + + + + + + diff --git a/ultimmc/cmake/UnitTest/test.rc b/ultimmc/cmake/UnitTest/test.rc new file mode 100644 index 0000000..9fe4147 --- /dev/null +++ b/ultimmc/cmake/UnitTest/test.rc @@ -0,0 +1,28 @@ +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +1 RT_MANIFEST "test.manifest" + +VS_VERSION_INFO VERSIONINFO +FILEVERSION 1,0,0,0 +FILEOS VOS_NT_WINDOWS32 +FILETYPE VFT_APP +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "000004b0" + BEGIN + VALUE "CompanyName", "MultiMC Contributors" + VALUE "FileDescription", "Testcase" + VALUE "FileVersion", "1.0.0.0" + VALUE "ProductName", "Launcher Testcase" + VALUE "ProductVersion", "5" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0000, 0x04b0 // Unicode + END +END diff --git a/ultimmc/cmake/UseJava.cmake b/ultimmc/cmake/UseJava.cmake new file mode 100644 index 0000000..1a5ef10 --- /dev/null +++ b/ultimmc/cmake/UseJava.cmake @@ -0,0 +1,881 @@ +# - Use Module for Java +# This file provides functions for Java. It is assumed that FindJava.cmake +# has already been loaded. See FindJava.cmake for information on how to +# load Java into your CMake project. +# +# add_jar(TARGET_NAME SRC1 SRC2 .. SRCN RCS1 RCS2 .. RCSN) +# +# This command creates a .jar. It compiles the given source +# files (SRC) and adds the given resource files (RCS) to the jar file. +# If only resource files are given then just a jar file is created. +# +# Additional instructions: +# To add compile flags to the target you can set these flags with +# the following variable: +# +# set(CMAKE_JAVA_COMPILE_FLAGS -nowarn) +# +# To add a path or a jar file to the class path you can do this +# with the CMAKE_JAVA_INCLUDE_PATH variable. +# +# set(CMAKE_JAVA_INCLUDE_PATH /usr/share/java/shibboleet.jar) +# +# To use a different output name for the target you can set it with: +# +# set(CMAKE_JAVA_TARGET_OUTPUT_NAME shibboleet.jar) +# add_jar(foobar foobar.java) +# +# To use a different output directory than CMAKE_CURRENT_BINARY_DIR +# you can set it with: +# +# set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${PROJECT_BINARY_DIR}/bin) +# +# To define an entry point in your jar you can set it with: +# +# set(CMAKE_JAVA_JAR_ENTRY_POINT com/examples/MyProject/Main) +# +# To add a VERSION to the target output name you can set it using +# CMAKE_JAVA_TARGET_VERSION. This will create a jar file with the name +# shibboleet-1.0.0.jar and will create a symlink shibboleet.jar +# pointing to the jar with the version information. +# +# set(CMAKE_JAVA_TARGET_VERSION 1.2.0) +# add_jar(shibboleet shibbotleet.java) +# +# If the target is a JNI library, utilize the following commands to +# create a JNI symbolic link: +# +# set(CMAKE_JNI_TARGET TRUE) +# set(CMAKE_JAVA_TARGET_VERSION 1.2.0) +# add_jar(shibboleet shibbotleet.java) +# install_jar(shibboleet ${LIB_INSTALL_DIR}/shibboleet) +# install_jni_symlink(shibboleet ${JAVA_LIB_INSTALL_DIR}) +# +# If a single target needs to produce more than one jar from its +# java source code, to prevent the accumulation of duplicate class +# files in subsequent jars, set/reset CMAKE_JAR_CLASSES_PREFIX prior +# to calling the add_jar() function: +# +# set(CMAKE_JAR_CLASSES_PREFIX com/redhat/foo) +# add_jar(foo foo.java) +# +# set(CMAKE_JAR_CLASSES_PREFIX com/redhat/bar) +# add_jar(bar bar.java) +# +# Target Properties: +# The add_jar() functions sets some target properties. You can get these +# properties with the +# get_property(TARGET PROPERTY ) +# command. +# +# INSTALL_FILES The files which should be installed. This is used by +# install_jar(). +# JNI_SYMLINK The JNI symlink which should be installed. +# This is used by install_jni_symlink(). +# JAR_FILE The location of the jar file so that you can include +# it. +# CLASS_DIR The directory where the class files can be found. For +# example to use them with javah. +# +# find_jar( +# name | NAMES name1 [name2 ...] +# [PATHS path1 [path2 ... ENV var]] +# [VERSIONS version1 [version2]] +# [DOC "cache documentation string"] +# ) +# +# This command is used to find a full path to the named jar. A cache +# entry named by is created to stor the result of this command. If +# the full path to a jar is found the result is stored in the variable +# and the search will not repeated unless the variable is cleared. If +# nothing is found, the result will be -NOTFOUND, and the search +# will be attempted again next time find_jar is invoked with the same +# variable. +# The name of the full path to a file that is searched for is specified +# by the names listed after NAMES argument. Additional search locations +# can be specified after the PATHS argument. If you require special a +# version of a jar file you can specify it with the VERSIONS argument. +# The argument after DOC will be used for the documentation string in +# the cache. +# +# install_jar(TARGET_NAME DESTINATION) +# +# This command installs the TARGET_NAME files to the given DESTINATION. +# It should be called in the same scope as add_jar() or it will fail. +# +# install_jni_symlink(TARGET_NAME DESTINATION) +# +# This command installs the TARGET_NAME JNI symlinks to the given +# DESTINATION. It should be called in the same scope as add_jar() +# or it will fail. +# +# create_javadoc( +# PACKAGES pkg1 [pkg2 ...] +# [SOURCEPATH ] +# [CLASSPATH ] +# [INSTALLPATH ] +# [DOCTITLE "the documentation title"] +# [WINDOWTITLE "the title of the document"] +# [AUTHOR TRUE|FALSE] +# [USE TRUE|FALSE] +# [VERSION TRUE|FALSE] +# ) +# +# Create java documentation based on files or packages. For more +# details please read the javadoc manpage. +# +# There are two main signatures for create_javadoc. The first +# signature works with package names on a path with source files: +# +# Example: +# create_javadoc(my_example_doc +# PACKAGES com.exmaple.foo com.example.bar +# SOURCEPATH "${CMAKE_CURRENT_SOURCE_DIR}" +# CLASSPATH ${CMAKE_JAVA_INCLUDE_PATH} +# WINDOWTITLE "My example" +# DOCTITLE "

My example

" +# AUTHOR TRUE +# USE TRUE +# VERSION TRUE +# ) +# +# The second signature for create_javadoc works on a given list of +# files. +# +# create_javadoc( +# FILES file1 [file2 ...] +# [CLASSPATH ] +# [INSTALLPATH ] +# [DOCTITLE "the documentation title"] +# [WINDOWTITLE "the title of the document"] +# [AUTHOR TRUE|FALSE] +# [USE TRUE|FALSE] +# [VERSION TRUE|FALSE] +# ) +# +# Example: +# create_javadoc(my_example_doc +# FILES ${example_SRCS} +# CLASSPATH ${CMAKE_JAVA_INCLUDE_PATH} +# WINDOWTITLE "My example" +# DOCTITLE "

My example

" +# AUTHOR TRUE +# USE TRUE +# VERSION TRUE +# ) +# +# Both signatures share most of the options. These options are the +# same as what you can find in the javadoc manpage. Please look at +# the manpage for CLASSPATH, DOCTITLE, WINDOWTITLE, AUTHOR, USE and +# VERSION. +# +# The documentation will be by default installed to +# +# ${CMAKE_INSTALL_PREFIX}/share/javadoc/ +# +# if you don't set the INSTALLPATH. +# + +#============================================================================= +# Copyright 2010-2011 Andreas schneider +# Copyright 2010 Ben Boeckel +# +# Distributed under the OSI-approved BSD License (the "License"); +# see accompanying file Copyright.txt for details. +# +# This software is distributed WITHOUT ANY WARRANTY; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the License for more information. +#============================================================================= +# (To distribute this file outside of CMake, substitute the full +# License text for the above reference.) + +function (__java_copy_file src dest comment) + add_custom_command( + OUTPUT ${dest} + COMMAND cmake -E copy_if_different + ARGS ${src} + ${dest} + DEPENDS ${src} + COMMENT ${comment}) +endfunction (__java_copy_file src dest comment) + +# define helper scripts +set(_JAVA_CLASS_FILELIST_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/UseJavaClassFilelist.cmake) +set(_JAVA_SYMLINK_SCRIPT ${CMAKE_CURRENT_LIST_DIR}/UseJavaSymlinks.cmake) + +function(add_jar _TARGET_NAME) + set(_JAVA_SOURCE_FILES ${ARGN}) + + if (NOT DEFINED CMAKE_JAVA_TARGET_OUTPUT_DIR) + set(CMAKE_JAVA_TARGET_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR}) + endif(NOT DEFINED CMAKE_JAVA_TARGET_OUTPUT_DIR) + + if (CMAKE_JAVA_JAR_ENTRY_POINT) + set(_ENTRY_POINT_OPTION e) + set(_ENTRY_POINT_VALUE ${CMAKE_JAVA_JAR_ENTRY_POINT}) + endif (CMAKE_JAVA_JAR_ENTRY_POINT) + + if (LIBRARY_OUTPUT_PATH) + set(CMAKE_JAVA_LIBRARY_OUTPUT_PATH ${LIBRARY_OUTPUT_PATH}) + else (LIBRARY_OUTPUT_PATH) + set(CMAKE_JAVA_LIBRARY_OUTPUT_PATH ${CMAKE_JAVA_TARGET_OUTPUT_DIR}) + endif (LIBRARY_OUTPUT_PATH) + + set(CMAKE_JAVA_INCLUDE_PATH + ${CMAKE_JAVA_INCLUDE_PATH} + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_JAVA_OBJECT_OUTPUT_PATH} + ${CMAKE_JAVA_LIBRARY_OUTPUT_PATH} + ) + + if (WIN32 AND NOT CYGWIN AND NOT CMAKE_CROSSCOMPILING) + set(CMAKE_JAVA_INCLUDE_FLAG_SEP ";") + else () + set(CMAKE_JAVA_INCLUDE_FLAG_SEP ":") + endif() + + foreach (JAVA_INCLUDE_DIR ${CMAKE_JAVA_INCLUDE_PATH}) + set(CMAKE_JAVA_INCLUDE_PATH_FINAL "${CMAKE_JAVA_INCLUDE_PATH_FINAL}${CMAKE_JAVA_INCLUDE_FLAG_SEP}${JAVA_INCLUDE_DIR}") + endforeach(JAVA_INCLUDE_DIR) + + set(CMAKE_JAVA_CLASS_OUTPUT_PATH "${CMAKE_JAVA_TARGET_OUTPUT_DIR}${CMAKE_FILES_DIRECTORY}/${_TARGET_NAME}.dir") + + set(_JAVA_TARGET_OUTPUT_NAME "${_TARGET_NAME}.jar") + if (CMAKE_JAVA_TARGET_OUTPUT_NAME AND CMAKE_JAVA_TARGET_VERSION) + set(_JAVA_TARGET_OUTPUT_NAME "${CMAKE_JAVA_TARGET_OUTPUT_NAME}-${CMAKE_JAVA_TARGET_VERSION}.jar") + set(_JAVA_TARGET_OUTPUT_LINK "${CMAKE_JAVA_TARGET_OUTPUT_NAME}.jar") + elseif (CMAKE_JAVA_TARGET_VERSION) + set(_JAVA_TARGET_OUTPUT_NAME "${_TARGET_NAME}-${CMAKE_JAVA_TARGET_VERSION}.jar") + set(_JAVA_TARGET_OUTPUT_LINK "${_TARGET_NAME}.jar") + elseif (CMAKE_JAVA_TARGET_OUTPUT_NAME) + set(_JAVA_TARGET_OUTPUT_NAME "${CMAKE_JAVA_TARGET_OUTPUT_NAME}.jar") + endif (CMAKE_JAVA_TARGET_OUTPUT_NAME AND CMAKE_JAVA_TARGET_VERSION) + # reset + set(CMAKE_JAVA_TARGET_OUTPUT_NAME) + + set(_JAVA_CLASS_FILES) + set(_JAVA_COMPILE_FILES) + set(_JAVA_DEPENDS) + set(_JAVA_RESOURCE_FILES) + foreach(_JAVA_SOURCE_FILE ${_JAVA_SOURCE_FILES}) + get_filename_component(_JAVA_EXT ${_JAVA_SOURCE_FILE} EXT) + get_filename_component(_JAVA_FILE ${_JAVA_SOURCE_FILE} NAME_WE) + get_filename_component(_JAVA_PATH ${_JAVA_SOURCE_FILE} PATH) + get_filename_component(_JAVA_FULL ${_JAVA_SOURCE_FILE} ABSOLUTE) + + file(RELATIVE_PATH _JAVA_REL_BINARY_PATH ${CMAKE_JAVA_TARGET_OUTPUT_DIR} ${_JAVA_FULL}) + file(RELATIVE_PATH _JAVA_REL_SOURCE_PATH ${CMAKE_CURRENT_SOURCE_DIR} ${_JAVA_FULL}) + string(LENGTH ${_JAVA_REL_BINARY_PATH} _BIN_LEN) + string(LENGTH ${_JAVA_REL_SOURCE_PATH} _SRC_LEN) + if (${_BIN_LEN} LESS ${_SRC_LEN}) + set(_JAVA_REL_PATH ${_JAVA_REL_BINARY_PATH}) + else (${_BIN_LEN} LESS ${_SRC_LEN}) + set(_JAVA_REL_PATH ${_JAVA_REL_SOURCE_PATH}) + endif (${_BIN_LEN} LESS ${_SRC_LEN}) + get_filename_component(_JAVA_REL_PATH ${_JAVA_REL_PATH} PATH) + + if (_JAVA_EXT MATCHES ".java") + list(APPEND _JAVA_COMPILE_FILES ${_JAVA_SOURCE_FILE}) + set(_JAVA_CLASS_FILE "${CMAKE_JAVA_CLASS_OUTPUT_PATH}/${_JAVA_REL_PATH}/${_JAVA_FILE}.class") + set(_JAVA_CLASS_FILES ${_JAVA_CLASS_FILES} ${_JAVA_CLASS_FILE}) + + elseif (_JAVA_EXT MATCHES ".jar" + OR _JAVA_EXT MATCHES ".war" + OR _JAVA_EXT MATCHES ".ear" + OR _JAVA_EXT MATCHES ".sar") + list(APPEND CMAKE_JAVA_INCLUDE_PATH ${_JAVA_SOURCE_FILE}) + + elseif (_JAVA_EXT STREQUAL "") + list(APPEND CMAKE_JAVA_INCLUDE_PATH ${JAVA_JAR_TARGET_${_JAVA_SOURCE_FILE}} ${JAVA_JAR_TARGET_${_JAVA_SOURCE_FILE}_CLASSPATH}) + list(APPEND _JAVA_DEPENDS ${JAVA_JAR_TARGET_${_JAVA_SOURCE_FILE}}) + + else (_JAVA_EXT MATCHES ".java") + __java_copy_file(${CMAKE_CURRENT_SOURCE_DIR}/${_JAVA_SOURCE_FILE} + ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/${_JAVA_SOURCE_FILE} + "Copying ${_JAVA_SOURCE_FILE} to the build directory") + list(APPEND _JAVA_RESOURCE_FILES ${_JAVA_SOURCE_FILE}) + endif (_JAVA_EXT MATCHES ".java") + endforeach(_JAVA_SOURCE_FILE) + + # create an empty java_class_filelist + if (NOT EXISTS ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist) + file(WRITE ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist "") + endif() + + if (_JAVA_COMPILE_FILES) + # Compile the java files and create a list of class files + add_custom_command( + # NOTE: this command generates an artificial dependency file + OUTPUT ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_compiled_${_TARGET_NAME} + COMMAND ${Java_JAVAC_EXECUTABLE} + ${CMAKE_JAVA_COMPILE_FLAGS} + -classpath "${CMAKE_JAVA_INCLUDE_PATH_FINAL}" + -d ${CMAKE_JAVA_CLASS_OUTPUT_PATH} + ${_JAVA_COMPILE_FILES} + COMMAND ${CMAKE_COMMAND} -E touch ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_compiled_${_TARGET_NAME} + DEPENDS ${_JAVA_COMPILE_FILES} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Building Java objects for ${_TARGET_NAME}.jar" + ) + add_custom_command( + OUTPUT ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist + COMMAND ${CMAKE_COMMAND} + -DCMAKE_JAVA_CLASS_OUTPUT_PATH=${CMAKE_JAVA_CLASS_OUTPUT_PATH} + -DCMAKE_JAR_CLASSES_PREFIX="${CMAKE_JAR_CLASSES_PREFIX}" + -P ${_JAVA_CLASS_FILELIST_SCRIPT} + DEPENDS ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_compiled_${_TARGET_NAME} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + endif (_JAVA_COMPILE_FILES) + + # create the jar file + set(_JAVA_JAR_OUTPUT_PATH + ${CMAKE_JAVA_TARGET_OUTPUT_DIR}/${_JAVA_TARGET_OUTPUT_NAME}) + if (CMAKE_JNI_TARGET) + add_custom_command( + OUTPUT ${_JAVA_JAR_OUTPUT_PATH} + COMMAND ${Java_JAR_EXECUTABLE} + -cf${_ENTRY_POINT_OPTION} ${_JAVA_JAR_OUTPUT_PATH} ${_ENTRY_POINT_VALUE} + ${_JAVA_RESOURCE_FILES} @java_class_filelist + COMMAND ${CMAKE_COMMAND} + -D_JAVA_TARGET_DIR=${CMAKE_JAVA_TARGET_OUTPUT_DIR} + -D_JAVA_TARGET_OUTPUT_NAME=${_JAVA_TARGET_OUTPUT_NAME} + -D_JAVA_TARGET_OUTPUT_LINK=${_JAVA_TARGET_OUTPUT_LINK} + -P ${_JAVA_SYMLINK_SCRIPT} + COMMAND ${CMAKE_COMMAND} + -D_JAVA_TARGET_DIR=${CMAKE_JAVA_TARGET_OUTPUT_DIR} + -D_JAVA_TARGET_OUTPUT_NAME=${_JAVA_JAR_OUTPUT_PATH} + -D_JAVA_TARGET_OUTPUT_LINK=${_JAVA_TARGET_OUTPUT_LINK} + -P ${_JAVA_SYMLINK_SCRIPT} + DEPENDS ${_JAVA_RESOURCE_FILES} ${_JAVA_DEPENDS} ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist + WORKING_DIRECTORY ${CMAKE_JAVA_CLASS_OUTPUT_PATH} + COMMENT "Creating Java archive ${_JAVA_TARGET_OUTPUT_NAME}" + ) + else () + add_custom_command( + OUTPUT ${_JAVA_JAR_OUTPUT_PATH} + COMMAND ${Java_JAR_EXECUTABLE} + -cf${_ENTRY_POINT_OPTION} ${_JAVA_JAR_OUTPUT_PATH} ${_ENTRY_POINT_VALUE} + ${_JAVA_RESOURCE_FILES} @java_class_filelist + COMMAND ${CMAKE_COMMAND} + -D_JAVA_TARGET_DIR=${CMAKE_JAVA_TARGET_OUTPUT_DIR} + -D_JAVA_TARGET_OUTPUT_NAME=${_JAVA_TARGET_OUTPUT_NAME} + -D_JAVA_TARGET_OUTPUT_LINK=${_JAVA_TARGET_OUTPUT_LINK} + -P ${_JAVA_SYMLINK_SCRIPT} + WORKING_DIRECTORY ${CMAKE_JAVA_CLASS_OUTPUT_PATH} + DEPENDS ${_JAVA_RESOURCE_FILES} ${_JAVA_DEPENDS} ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist + COMMENT "Creating Java archive ${_JAVA_TARGET_OUTPUT_NAME}" + ) + endif (CMAKE_JNI_TARGET) + + # Add the target and make sure we have the latest resource files. + add_custom_target(${_TARGET_NAME} ALL DEPENDS ${_JAVA_JAR_OUTPUT_PATH}) + + set_property( + TARGET + ${_TARGET_NAME} + PROPERTY + INSTALL_FILES + ${_JAVA_JAR_OUTPUT_PATH} + ) + + if (_JAVA_TARGET_OUTPUT_LINK) + set_property( + TARGET + ${_TARGET_NAME} + PROPERTY + INSTALL_FILES + ${_JAVA_JAR_OUTPUT_PATH} + ${CMAKE_JAVA_TARGET_OUTPUT_DIR}/${_JAVA_TARGET_OUTPUT_LINK} + ) + + if (CMAKE_JNI_TARGET) + set_property( + TARGET + ${_TARGET_NAME} + PROPERTY + JNI_SYMLINK + ${CMAKE_JAVA_TARGET_OUTPUT_DIR}/${_JAVA_TARGET_OUTPUT_LINK} + ) + endif (CMAKE_JNI_TARGET) + endif (_JAVA_TARGET_OUTPUT_LINK) + + set_property( + TARGET + ${_TARGET_NAME} + PROPERTY + JAR_FILE + ${_JAVA_JAR_OUTPUT_PATH} + ) + + set_property( + TARGET + ${_TARGET_NAME} + PROPERTY + CLASSDIR + ${CMAKE_JAVA_CLASS_OUTPUT_PATH} + ) + +endfunction(add_jar) + +function(INSTALL_JAR _TARGET_NAME _DESTINATION) + get_property(__FILES + TARGET + ${_TARGET_NAME} + PROPERTY + INSTALL_FILES + ) + + if (__FILES) + install( + FILES + ${__FILES} + DESTINATION + ${_DESTINATION} + ) + else (__FILES) + message(SEND_ERROR "The target ${_TARGET_NAME} is not known in this scope.") + endif (__FILES) +endfunction(INSTALL_JAR _TARGET_NAME _DESTINATION) + +function(INSTALL_JNI_SYMLINK _TARGET_NAME _DESTINATION) + get_property(__SYMLINK + TARGET + ${_TARGET_NAME} + PROPERTY + JNI_SYMLINK + ) + + if (__SYMLINK) + install( + FILES + ${__SYMLINK} + DESTINATION + ${_DESTINATION} + ) + else (__SYMLINK) + message(SEND_ERROR "The target ${_TARGET_NAME} is not known in this scope.") + endif (__SYMLINK) +endfunction(INSTALL_JNI_SYMLINK _TARGET_NAME _DESTINATION) + +function (find_jar VARIABLE) + set(_jar_names) + set(_jar_files) + set(_jar_versions) + set(_jar_paths + /usr/share/java/ + /usr/local/share/java/ + ${Java_JAR_PATHS}) + set(_jar_doc "NOTSET") + + set(_state "name") + + foreach (arg ${ARGN}) + if (${_state} STREQUAL "name") + if (${arg} STREQUAL "VERSIONS") + set(_state "versions") + elseif (${arg} STREQUAL "NAMES") + set(_state "names") + elseif (${arg} STREQUAL "PATHS") + set(_state "paths") + elseif (${arg} STREQUAL "DOC") + set(_state "doc") + else (${arg} STREQUAL "NAMES") + set(_jar_names ${arg}) + if (_jar_doc STREQUAL "NOTSET") + set(_jar_doc "Finding ${arg} jar") + endif (_jar_doc STREQUAL "NOTSET") + endif (${arg} STREQUAL "VERSIONS") + elseif (${_state} STREQUAL "versions") + if (${arg} STREQUAL "NAMES") + set(_state "names") + elseif (${arg} STREQUAL "PATHS") + set(_state "paths") + elseif (${arg} STREQUAL "DOC") + set(_state "doc") + else (${arg} STREQUAL "NAMES") + set(_jar_versions ${_jar_versions} ${arg}) + endif (${arg} STREQUAL "NAMES") + elseif (${_state} STREQUAL "names") + if (${arg} STREQUAL "VERSIONS") + set(_state "versions") + elseif (${arg} STREQUAL "PATHS") + set(_state "paths") + elseif (${arg} STREQUAL "DOC") + set(_state "doc") + else (${arg} STREQUAL "VERSIONS") + set(_jar_names ${_jar_names} ${arg}) + if (_jar_doc STREQUAL "NOTSET") + set(_jar_doc "Finding ${arg} jar") + endif (_jar_doc STREQUAL "NOTSET") + endif (${arg} STREQUAL "VERSIONS") + elseif (${_state} STREQUAL "paths") + if (${arg} STREQUAL "VERSIONS") + set(_state "versions") + elseif (${arg} STREQUAL "NAMES") + set(_state "names") + elseif (${arg} STREQUAL "DOC") + set(_state "doc") + else (${arg} STREQUAL "VERSIONS") + set(_jar_paths ${_jar_paths} ${arg}) + endif (${arg} STREQUAL "VERSIONS") + elseif (${_state} STREQUAL "doc") + if (${arg} STREQUAL "VERSIONS") + set(_state "versions") + elseif (${arg} STREQUAL "NAMES") + set(_state "names") + elseif (${arg} STREQUAL "PATHS") + set(_state "paths") + else (${arg} STREQUAL "VERSIONS") + set(_jar_doc ${arg}) + endif (${arg} STREQUAL "VERSIONS") + endif (${_state} STREQUAL "name") + endforeach (arg ${ARGN}) + + if (NOT _jar_names) + message(FATAL_ERROR "find_jar: No name to search for given") + endif (NOT _jar_names) + + foreach (jar_name ${_jar_names}) + foreach (version ${_jar_versions}) + set(_jar_files ${_jar_files} ${jar_name}-${version}.jar) + endforeach (version ${_jar_versions}) + set(_jar_files ${_jar_files} ${jar_name}.jar) + endforeach (jar_name ${_jar_names}) + + find_file(${VARIABLE} + NAMES ${_jar_files} + PATHS ${_jar_paths} + DOC ${_jar_doc} + NO_DEFAULT_PATH) +endfunction (find_jar VARIABLE) + +function(create_javadoc _target) + set(_javadoc_packages) + set(_javadoc_files) + set(_javadoc_sourcepath) + set(_javadoc_classpath) + set(_javadoc_installpath "${CMAKE_INSTALL_PREFIX}/share/javadoc") + set(_javadoc_doctitle) + set(_javadoc_windowtitle) + set(_javadoc_author FALSE) + set(_javadoc_version FALSE) + set(_javadoc_use FALSE) + + set(_state "package") + + foreach (arg ${ARGN}) + if (${_state} STREQUAL "package") + if (${arg} STREQUAL "PACKAGES") + set(_state "packages") + elseif (${arg} STREQUAL "FILES") + set(_state "files") + elseif (${arg} STREQUAL "SOURCEPATH") + set(_state "sourcepath") + elseif (${arg} STREQUAL "CLASSPATH") + set(_state "classpath") + elseif (${arg} STREQUAL "INSTALLPATH") + set(_state "installpath") + elseif (${arg} STREQUAL "DOCTITLE") + set(_state "doctitle") + elseif (${arg} STREQUAL "WINDOWTITLE") + set(_state "windowtitle") + elseif (${arg} STREQUAL "AUTHOR") + set(_state "author") + elseif (${arg} STREQUAL "USE") + set(_state "use") + elseif (${arg} STREQUAL "VERSION") + set(_state "version") + else () + set(_javadoc_packages ${arg}) + set(_state "packages") + endif () + elseif (${_state} STREQUAL "packages") + if (${arg} STREQUAL "FILES") + set(_state "files") + elseif (${arg} STREQUAL "SOURCEPATH") + set(_state "sourcepath") + elseif (${arg} STREQUAL "CLASSPATH") + set(_state "classpath") + elseif (${arg} STREQUAL "INSTALLPATH") + set(_state "installpath") + elseif (${arg} STREQUAL "DOCTITLE") + set(_state "doctitle") + elseif (${arg} STREQUAL "WINDOWTITLE") + set(_state "windowtitle") + elseif (${arg} STREQUAL "AUTHOR") + set(_state "author") + elseif (${arg} STREQUAL "USE") + set(_state "use") + elseif (${arg} STREQUAL "VERSION") + set(_state "version") + else () + list(APPEND _javadoc_packages ${arg}) + endif () + elseif (${_state} STREQUAL "files") + if (${arg} STREQUAL "PACKAGES") + set(_state "packages") + elseif (${arg} STREQUAL "SOURCEPATH") + set(_state "sourcepath") + elseif (${arg} STREQUAL "CLASSPATH") + set(_state "classpath") + elseif (${arg} STREQUAL "INSTALLPATH") + set(_state "installpath") + elseif (${arg} STREQUAL "DOCTITLE") + set(_state "doctitle") + elseif (${arg} STREQUAL "WINDOWTITLE") + set(_state "windowtitle") + elseif (${arg} STREQUAL "AUTHOR") + set(_state "author") + elseif (${arg} STREQUAL "USE") + set(_state "use") + elseif (${arg} STREQUAL "VERSION") + set(_state "version") + else () + list(APPEND _javadoc_files ${arg}) + endif () + elseif (${_state} STREQUAL "sourcepath") + if (${arg} STREQUAL "PACKAGES") + set(_state "packages") + elseif (${arg} STREQUAL "FILES") + set(_state "files") + elseif (${arg} STREQUAL "CLASSPATH") + set(_state "classpath") + elseif (${arg} STREQUAL "INSTALLPATH") + set(_state "installpath") + elseif (${arg} STREQUAL "DOCTITLE") + set(_state "doctitle") + elseif (${arg} STREQUAL "WINDOWTITLE") + set(_state "windowtitle") + elseif (${arg} STREQUAL "AUTHOR") + set(_state "author") + elseif (${arg} STREQUAL "USE") + set(_state "use") + elseif (${arg} STREQUAL "VERSION") + set(_state "version") + else () + list(APPEND _javadoc_sourcepath ${arg}) + endif () + elseif (${_state} STREQUAL "classpath") + if (${arg} STREQUAL "PACKAGES") + set(_state "packages") + elseif (${arg} STREQUAL "FILES") + set(_state "files") + elseif (${arg} STREQUAL "SOURCEPATH") + set(_state "sourcepath") + elseif (${arg} STREQUAL "INSTALLPATH") + set(_state "installpath") + elseif (${arg} STREQUAL "DOCTITLE") + set(_state "doctitle") + elseif (${arg} STREQUAL "WINDOWTITLE") + set(_state "windowtitle") + elseif (${arg} STREQUAL "AUTHOR") + set(_state "author") + elseif (${arg} STREQUAL "USE") + set(_state "use") + elseif (${arg} STREQUAL "VERSION") + set(_state "version") + else () + list(APPEND _javadoc_classpath ${arg}) + endif () + elseif (${_state} STREQUAL "installpath") + if (${arg} STREQUAL "PACKAGES") + set(_state "packages") + elseif (${arg} STREQUAL "FILES") + set(_state "files") + elseif (${arg} STREQUAL "SOURCEPATH") + set(_state "sourcepath") + elseif (${arg} STREQUAL "DOCTITLE") + set(_state "doctitle") + elseif (${arg} STREQUAL "WINDOWTITLE") + set(_state "windowtitle") + elseif (${arg} STREQUAL "AUTHOR") + set(_state "author") + elseif (${arg} STREQUAL "USE") + set(_state "use") + elseif (${arg} STREQUAL "VERSION") + set(_state "version") + else () + set(_javadoc_installpath ${arg}) + endif () + elseif (${_state} STREQUAL "doctitle") + if (${arg} STREQUAL "PACKAGES") + set(_state "packages") + elseif (${arg} STREQUAL "FILES") + set(_state "files") + elseif (${arg} STREQUAL "SOURCEPATH") + set(_state "sourcepath") + elseif (${arg} STREQUAL "INSTALLPATH") + set(_state "installpath") + elseif (${arg} STREQUAL "CLASSPATH") + set(_state "classpath") + elseif (${arg} STREQUAL "WINDOWTITLE") + set(_state "windowtitle") + elseif (${arg} STREQUAL "AUTHOR") + set(_state "author") + elseif (${arg} STREQUAL "USE") + set(_state "use") + elseif (${arg} STREQUAL "VERSION") + set(_state "version") + else () + set(_javadoc_doctitle ${arg}) + endif () + elseif (${_state} STREQUAL "windowtitle") + if (${arg} STREQUAL "PACKAGES") + set(_state "packages") + elseif (${arg} STREQUAL "FILES") + set(_state "files") + elseif (${arg} STREQUAL "SOURCEPATH") + set(_state "sourcepath") + elseif (${arg} STREQUAL "CLASSPATH") + set(_state "classpath") + elseif (${arg} STREQUAL "INSTALLPATH") + set(_state "installpath") + elseif (${arg} STREQUAL "DOCTITLE") + set(_state "doctitle") + elseif (${arg} STREQUAL "AUTHOR") + set(_state "author") + elseif (${arg} STREQUAL "USE") + set(_state "use") + elseif (${arg} STREQUAL "VERSION") + set(_state "version") + else () + set(_javadoc_windowtitle ${arg}) + endif () + elseif (${_state} STREQUAL "author") + if (${arg} STREQUAL "PACKAGES") + set(_state "packages") + elseif (${arg} STREQUAL "FILES") + set(_state "files") + elseif (${arg} STREQUAL "SOURCEPATH") + set(_state "sourcepath") + elseif (${arg} STREQUAL "CLASSPATH") + set(_state "classpath") + elseif (${arg} STREQUAL "INSTALLPATH") + set(_state "installpath") + elseif (${arg} STREQUAL "DOCTITLE") + set(_state "doctitle") + elseif (${arg} STREQUAL "WINDOWTITLE") + set(_state "windowtitle") + elseif (${arg} STREQUAL "AUTHOR") + set(_state "author") + elseif (${arg} STREQUAL "USE") + set(_state "use") + elseif (${arg} STREQUAL "VERSION") + set(_state "version") + else () + set(_javadoc_author ${arg}) + endif () + elseif (${_state} STREQUAL "use") + if (${arg} STREQUAL "PACKAGES") + set(_state "packages") + elseif (${arg} STREQUAL "FILES") + set(_state "files") + elseif (${arg} STREQUAL "SOURCEPATH") + set(_state "sourcepath") + elseif (${arg} STREQUAL "CLASSPATH") + set(_state "classpath") + elseif (${arg} STREQUAL "INSTALLPATH") + set(_state "installpath") + elseif (${arg} STREQUAL "DOCTITLE") + set(_state "doctitle") + elseif (${arg} STREQUAL "WINDOWTITLE") + set(_state "windowtitle") + elseif (${arg} STREQUAL "AUTHOR") + set(_state "author") + elseif (${arg} STREQUAL "USE") + set(_state "use") + elseif (${arg} STREQUAL "VERSION") + set(_state "version") + else () + set(_javadoc_use ${arg}) + endif () + elseif (${_state} STREQUAL "version") + if (${arg} STREQUAL "PACKAGES") + set(_state "packages") + elseif (${arg} STREQUAL "FILES") + set(_state "files") + elseif (${arg} STREQUAL "SOURCEPATH") + set(_state "sourcepath") + elseif (${arg} STREQUAL "CLASSPATH") + set(_state "classpath") + elseif (${arg} STREQUAL "INSTALLPATH") + set(_state "installpath") + elseif (${arg} STREQUAL "DOCTITLE") + set(_state "doctitle") + elseif (${arg} STREQUAL "WINDOWTITLE") + set(_state "windowtitle") + elseif (${arg} STREQUAL "AUTHOR") + set(_state "author") + elseif (${arg} STREQUAL "USE") + set(_state "use") + elseif (${arg} STREQUAL "VERSION") + set(_state "version") + else () + set(_javadoc_version ${arg}) + endif () + endif (${_state} STREQUAL "package") + endforeach (arg ${ARGN}) + + set(_javadoc_builddir ${CMAKE_CURRENT_BINARY_DIR}/javadoc/${_target}) + set(_javadoc_options -d ${_javadoc_builddir}) + + if (_javadoc_sourcepath) + set(_start TRUE) + foreach(_path ${_javadoc_sourcepath}) + if (_start) + set(_sourcepath ${_path}) + set(_start FALSE) + else (_start) + set(_sourcepath ${_sourcepath}:${_path}) + endif (_start) + endforeach(_path ${_javadoc_sourcepath}) + set(_javadoc_options ${_javadoc_options} -sourcepath ${_sourcepath}) + endif (_javadoc_sourcepath) + + if (_javadoc_classpath) + set(_start TRUE) + foreach(_path ${_javadoc_classpath}) + if (_start) + set(_classpath ${_path}) + set(_start FALSE) + else (_start) + set(_classpath ${_classpath}:${_path}) + endif (_start) + endforeach(_path ${_javadoc_classpath}) + set(_javadoc_options ${_javadoc_options} -classpath "${_classpath}") + endif (_javadoc_classpath) + + if (_javadoc_doctitle) + set(_javadoc_options ${_javadoc_options} -doctitle '${_javadoc_doctitle}') + endif (_javadoc_doctitle) + + if (_javadoc_windowtitle) + set(_javadoc_options ${_javadoc_options} -windowtitle '${_javadoc_windowtitle}') + endif (_javadoc_windowtitle) + + if (_javadoc_author) + set(_javadoc_options ${_javadoc_options} -author) + endif (_javadoc_author) + + if (_javadoc_use) + set(_javadoc_options ${_javadoc_options} -use) + endif (_javadoc_use) + + if (_javadoc_version) + set(_javadoc_options ${_javadoc_options} -version) + endif (_javadoc_version) + + add_custom_target(${_target}_javadoc ALL + COMMAND ${Java_JAVADOC_EXECUTABLE} ${_javadoc_options} + ${_javadoc_files} + ${_javadoc_packages} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + ) + + install( + DIRECTORY ${_javadoc_builddir} + DESTINATION ${_javadoc_installpath} + ) +endfunction(create_javadoc) diff --git a/ultimmc/cmake/UseJavaClassFilelist.cmake b/ultimmc/cmake/UseJavaClassFilelist.cmake new file mode 100644 index 0000000..c842bf7 --- /dev/null +++ b/ultimmc/cmake/UseJavaClassFilelist.cmake @@ -0,0 +1,52 @@ +# +# This script create a list of compiled Java class files to be added to a +# jar file. This avoids including cmake files which get created in the +# binary directory. +# + +#============================================================================= +# Copyright 2010-2011 Andreas schneider +# +# Distributed under the OSI-approved BSD License (the "License"); +# see accompanying file Copyright.txt for details. +# +# This software is distributed WITHOUT ANY WARRANTY; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the License for more information. +#============================================================================= +# (To distribute this file outside of CMake, substitute the full +# License text for the above reference.) + +if (CMAKE_JAVA_CLASS_OUTPUT_PATH) + if (EXISTS "${CMAKE_JAVA_CLASS_OUTPUT_PATH}") + + set(_JAVA_GLOBBED_FILES) + if (CMAKE_JAR_CLASSES_PREFIX) + foreach(JAR_CLASS_PREFIX ${CMAKE_JAR_CLASSES_PREFIX}) + message(STATUS "JAR_CLASS_PREFIX: ${JAR_CLASS_PREFIX}") + + file(GLOB_RECURSE _JAVA_GLOBBED_TMP_FILES "${CMAKE_JAVA_CLASS_OUTPUT_PATH}/${JAR_CLASS_PREFIX}/*.class") + if (_JAVA_GLOBBED_TMP_FILES) + list(APPEND _JAVA_GLOBBED_FILES ${_JAVA_GLOBBED_TMP_FILES}) + endif (_JAVA_GLOBBED_TMP_FILES) + endforeach(JAR_CLASS_PREFIX ${CMAKE_JAR_CLASSES_PREFIX}) + else() + file(GLOB_RECURSE _JAVA_GLOBBED_FILES "${CMAKE_JAVA_CLASS_OUTPUT_PATH}/*.class") + endif (CMAKE_JAR_CLASSES_PREFIX) + + set(_JAVA_CLASS_FILES) + # file(GLOB_RECURSE foo RELATIVE) is broken so we need this. + foreach(_JAVA_GLOBBED_FILE ${_JAVA_GLOBBED_FILES}) + file(RELATIVE_PATH _JAVA_CLASS_FILE ${CMAKE_JAVA_CLASS_OUTPUT_PATH} ${_JAVA_GLOBBED_FILE}) + set(_JAVA_CLASS_FILES ${_JAVA_CLASS_FILES}${_JAVA_CLASS_FILE}\n) + endforeach(_JAVA_GLOBBED_FILE ${_JAVA_GLOBBED_FILES}) + + # write to file + file(WRITE ${CMAKE_JAVA_CLASS_OUTPUT_PATH}/java_class_filelist ${_JAVA_CLASS_FILES}) + + else (EXISTS "${CMAKE_JAVA_CLASS_OUTPUT_PATH}") + message(SEND_ERROR "FATAL: Java class output path doesn't exist") + endif (EXISTS "${CMAKE_JAVA_CLASS_OUTPUT_PATH}") +else (CMAKE_JAVA_CLASS_OUTPUT_PATH) + message(SEND_ERROR "FATAL: Can't find CMAKE_JAVA_CLASS_OUTPUT_PATH") +endif (CMAKE_JAVA_CLASS_OUTPUT_PATH) diff --git a/ultimmc/cmake/UseJavaSymlinks.cmake b/ultimmc/cmake/UseJavaSymlinks.cmake new file mode 100644 index 0000000..c66ee1e --- /dev/null +++ b/ultimmc/cmake/UseJavaSymlinks.cmake @@ -0,0 +1,32 @@ +# +# Helper script for UseJava.cmake +# + +#============================================================================= +# Copyright 2010-2011 Andreas schneider +# +# Distributed under the OSI-approved BSD License (the "License"); +# see accompanying file Copyright.txt for details. +# +# This software is distributed WITHOUT ANY WARRANTY; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the License for more information. +#============================================================================= +# (To distribute this file outside of CMake, substitute the full +# License text for the above reference.) + +if (UNIX AND _JAVA_TARGET_OUTPUT_LINK) + if (_JAVA_TARGET_OUTPUT_NAME) + find_program(LN_EXECUTABLE + NAMES + ln + ) + + execute_process( + COMMAND ${LN_EXECUTABLE} -sf "${_JAVA_TARGET_OUTPUT_NAME}" "${_JAVA_TARGET_OUTPUT_LINK}" + WORKING_DIRECTORY ${_JAVA_TARGET_DIR} + ) + else (_JAVA_TARGET_OUTPUT_NAME) + message(SEND_ERROR "FATAL: Can't find _JAVA_TARGET_OUTPUT_NAME") + endif (_JAVA_TARGET_OUTPUT_NAME) +endif (UNIX AND _JAVA_TARGET_OUTPUT_LINK) diff --git a/ultimmc/doc/multimc.1.txt b/ultimmc/doc/multimc.1.txt new file mode 100644 index 0000000..da65af2 --- /dev/null +++ b/ultimmc/doc/multimc.1.txt @@ -0,0 +1,64 @@ +MULTIMC(1) +========== +:doctype: manpage + + +NAME +---- +multimc - a launcher and instance manager for Minecraft. + + +SYNOPSIS +-------- +*multimc* ['OPTIONS'] + + +DESCRIPTION +----------- +MultiMC is a custom launcher for Minecraft that allows you to easily manage +multiple installations of Minecraft at once. It also allows you to easily +install and remove mods by simply dragging and dropping. +Here are the current features of MultiMC. + +OPTIONS +------- +*-d, --dir*='DIRECTORY':: + Use 'DIRECTORY' as the MultiMC root. + +*-l, --launch*='INSTANCE_ID':: + Launch the instance specified by 'INSTANCE_ID'. + +*--alive*:: + Write a small 'live.check' file after MultiMC starts. + +*-h, --help*:: + Display help text and exit. + +*-v, --version*:: + Display program version and exit. +*-a, --profile*='PROFILE':: + Use the account specified by 'PROFILE' (only valid in combination with --launch). + +EXIT STATUS +----------- +*0*:: + Success + +*1*:: + Failure (syntax or usage error; configuration error; unexpected error). + +BUGS +---- + + +RESOURCES +--------- +GitHub: + +Main website: + +AUTHORS +------- +peterix + +// vim: syntax=asciidoc diff --git a/ultimmc/launcher/Application.cpp b/ultimmc/launcher/Application.cpp new file mode 100644 index 0000000..ac255ab --- /dev/null +++ b/ultimmc/launcher/Application.cpp @@ -0,0 +1,1755 @@ +#include "Application.h" +#include "BuildConfig.h" + +#include "ui/MainWindow.h" +#include "ui/InstanceWindow.h" + +#include "ui/instanceview/AccessibleInstanceView.h" + +#include "ui/pages/BasePageProvider.h" +#include "ui/pages/global/LauncherPage.h" +#include "ui/pages/global/MinecraftPage.h" +#include "ui/pages/global/JavaPage.h" +#include "ui/pages/global/LanguagePage.h" +#include "ui/pages/global/ProxyPage.h" +#include "ui/pages/global/ExternalToolsPage.h" +#include "ui/pages/global/AccountListPage.h" +#include "ui/pages/global/PasteEEPage.h" +#include "ui/pages/global/CustomCommandsPage.h" + +#include "ui/themes/ITheme.h" +#include "ui/themes/SystemTheme.h" +#include "ui/themes/DarkTheme.h" +#include "ui/themes/BrightTheme.h" +#include "ui/themes/CustomTheme.h" + +#include "ui/setupwizard/SetupWizard.h" +#include "ui/setupwizard/LanguageWizardPage.h" +#include "ui/setupwizard/JavaWizardPage.h" +#include "ui/setupwizard/AnalyticsWizardPage.h" + +#include "ui/dialogs/CustomMessageBox.h" + +#include "ui/pagedialog/PageDialog.h" + +#include "ApplicationMessage.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "InstanceList.h" + +#include +#include +#include "icons/IconList.h" +#include "net/HttpMetaCache.h" + +#include "java/JavaUtils.h" + +#include "updater/UpdateChecker.h" + +#include "tools/JProfiler.h" +#include "tools/JVisualVM.h" +#include "tools/MCEditTool.h" +#include "AuthServer.h" + +#include +#include "settings/INISettingsObject.h" +#include "settings/Setting.h" + +#include "translations/TranslationsModel.h" +#include "meta/Index.h" + +#include +#include +#include +#include + +#include +#include + +#include + + +#if defined Q_OS_WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include +#endif + +#define STRINGIFY(x) #x +#define TOSTRING(x) STRINGIFY(x) + +static const QLatin1String liveCheckFile("live.check"); + +using namespace Commandline; + +#define MACOS_HINT "If you are on macOS Sierra, you might have to move the app to your /Applications or ~/Applications folder. "\ + "This usually fixes the problem and you can move the application elsewhere afterwards.\n"\ + "\n" + +namespace { +void appDebugOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + const char *levels = "DWCFIS"; + const QString format("%1 %2 %3\n"); + + qint64 msecstotal = APPLICATION->timeSinceStart(); + qint64 seconds = msecstotal / 1000; + qint64 msecs = msecstotal % 1000; + QString foo; + char buf[1025] = {0}; + ::snprintf(buf, 1024, "%5lld.%03lld", seconds, msecs); + + QString out = format.arg(buf).arg(levels[type]).arg(msg); + + APPLICATION->logFile->write(out.toUtf8()); + APPLICATION->logFile->flush(); + QTextStream(stderr) << out.toLocal8Bit(); + fflush(stderr); +} + +QString getIdealPlatform(QString currentPlatform) { + auto info = Sys::getKernelInfo(); + switch(info.kernelType) { + case Sys::KernelType::Darwin: { + if(info.kernelMajor >= 17) { + // macOS 10.13 or newer + return "osx64-5.15.2"; + } + else { + // macOS 10.12 or older + return "osx64"; + } + } + case Sys::KernelType::Windows: { + // FIXME: 5.15.2 is not stable on Windows, due to a large number of completely unpredictable and hard to reproduce issues + break; +/* + if(info.kernelMajor == 6 && info.kernelMinor >= 1) { + // Windows 7 + return "win32-5.15.2"; + } + else if (info.kernelMajor > 6) { + // Above Windows 7 + return "win32-5.15.2"; + } + else { + // Below Windows 7 + return "win32"; + } +*/ + } + case Sys::KernelType::Undetermined: + case Sys::KernelType::Linux: { + break; + } + } + return currentPlatform; +} + +} + +Application::Application(int &argc, char **argv) : QApplication(argc, argv) +{ +#if defined Q_OS_WIN32 + // attach the parent console + if(AttachConsole(ATTACH_PARENT_PROCESS)) + { + // if attach succeeds, reopen and sync all the i/o + if(freopen("CON", "w", stdout)) + { + std::cout.sync_with_stdio(); + } + if(freopen("CON", "w", stderr)) + { + std::cerr.sync_with_stdio(); + } + if(freopen("CON", "r", stdin)) + { + std::cin.sync_with_stdio(); + } + auto out = GetStdHandle (STD_OUTPUT_HANDLE); + DWORD written; + const char * endline = "\n"; + WriteConsole(out, endline, strlen(endline), &written, NULL); + consoleAttached = true; + } +#endif + setOrganizationName(BuildConfig.LAUNCHER_NAME); + setOrganizationDomain(BuildConfig.LAUNCHER_DOMAIN); + setApplicationName(BuildConfig.LAUNCHER_NAME); + setApplicationDisplayName(BuildConfig.LAUNCHER_DISPLAYNAME); + setApplicationVersion(BuildConfig.printableVersionString()); + + startTime = QDateTime::currentDateTime(); + +#ifdef Q_OS_LINUX + { + QFile osrelease("/proc/sys/kernel/osrelease"); + if (osrelease.open(QFile::ReadOnly | QFile::Text)) { + QTextStream in(&osrelease); + auto contents = in.readAll(); + if( + contents.contains("WSL", Qt::CaseInsensitive) || + contents.contains("Microsoft", Qt::CaseInsensitive) + ) { + showFatalErrorMessage( + "Unsupported system detected!", + "Linux-on-Windows distributions are not supported.\n\n" + "Please use the Windows binary when playing on Windows." + ); + return; + } + } + } +#endif + + // Don't quit on hiding the last window + this->setQuitOnLastWindowClosed(false); + + // Commandline parsing + QHash args; + { + Parser parser(FlagStyle::GNU, ArgumentStyle::SpaceAndEquals); + + // --help + parser.addSwitch("help"); + parser.addShortOpt("help", 'h'); + parser.addDocumentation("help", "Display this help and exit."); + // --version + parser.addSwitch("version"); + parser.addShortOpt("version", 'V'); + parser.addDocumentation("version", "Display program version and exit."); + // --dir + parser.addOption("dir"); + parser.addShortOpt("dir", 'd'); + parser.addDocumentation("dir", "Use the supplied folder as application root instead of the binary location (use '.' for current)"); + // --launch + parser.addOption("launch"); + parser.addShortOpt("launch", 'l'); + parser.addDocumentation("launch", "Launch the specified instance (by instance ID)"); + // --server + parser.addOption("server"); + parser.addShortOpt("server", 's'); + parser.addDocumentation("server", "Join the specified server on launch (only valid in combination with --launch, mutually exclusive with --world)"); + // --world + parser.addOption("world"); + parser.addShortOpt("world", 'w'); + parser.addDocumentation("world", "Join the singleplayer world with the specified folder name on launch (only valid in combination with --launch, mutually exclusive with --server, only works with Minecraft 23w14a and later)"); + // --profile + parser.addOption("profile"); + parser.addShortOpt("profile", 'a'); + parser.addDocumentation("profile", "Use the account specified by its profile name (only valid in combination with --launch)"); + // --offline + parser.addSwitch("offline"); + parser.addShortOpt("offline", 'o'); + parser.addDocumentation("offline", "Launch offline (only valid in combination with --launch)"); + // --name + parser.addOption("name"); + parser.addShortOpt("name", 'n'); + parser.addDocumentation("name", "When launching offline, use specified name (only makes sense in combination with --launch and --offline)"); + // --alive + parser.addSwitch("alive"); + parser.addDocumentation("alive", "Write a small '" + liveCheckFile + "' file after the launcher starts"); + // --import + parser.addOption("import"); + parser.addShortOpt("import", 'I'); + parser.addDocumentation("import", "Import instance from specified zip (local path or URL)"); + + // parse the arguments + try + { + args = parser.parse(arguments()); + } + catch (const ParsingError &e) + { + std::cerr << "CommandLineError: " << e.what() << std::endl; + if(argc > 0) + std::cerr << "Try '" << argv[0] << " -h' to get help on command line parameters." + << std::endl; + m_status = Application::Failed; + return; + } + + // display help and exit + if (args["help"].toBool()) + { + std::cout << qPrintable(parser.compileHelp(arguments()[0])); + m_status = Application::Succeeded; + return; + } + + // display version and exit + if (args["version"].toBool()) + { + std::cout << "Version " << BuildConfig.printableVersionString().toStdString() << std::endl; + std::cout << "Git " << BuildConfig.GIT_COMMIT.toStdString() << std::endl; + m_status = Application::Succeeded; + return; + } + } + m_instanceIdToLaunch = args["launch"].toString(); + m_serverToJoin = args["server"].toString(); + m_worldToJoin = args["world"].toString(); + m_profileToUse = args["profile"].toString(); + if(args["offline"].toBool()) { + m_offline = true; + m_offlineName = args["name"].toString(); + } + m_liveCheck = args["alive"].toBool(); + m_zipToImport = args["import"].toUrl(); + + QString origcwdPath = QDir::currentPath(); + QString binPath = applicationDirPath(); + QString adjustedBy; + QString dataPath; + // change folder + QString dirParam = args["dir"].toString(); + if (!dirParam.isEmpty()) + { + // the dir param. it makes multimc data path point to whatever the user specified + // on command line + adjustedBy += "Command line " + dirParam; + dataPath = dirParam; + } + else + { +#if defined(Q_OS_MAC) + QDir foo(FS::PathCombine(applicationDirPath(), "../../Data")); + dataPath = foo.absolutePath(); + adjustedBy += "Fallback to special Mac location " + dataPath; +#else + dataPath = applicationDirPath(); + adjustedBy += "Fallback to binary path " + dataPath; +#endif + } + + if (!FS::ensureFolderPathExists(dataPath)) + { + showFatalErrorMessage( + "The launcher data folder could not be created.", + QString( + "The launcher data folder could not be created.\n" + "\n" +#if defined(Q_OS_MAC) + MACOS_HINT +#endif + "Make sure you have the right permissions to the launcher data folder and any folder needed to access it.\n" + "(%1)\n" + "\n" + "The launcher cannot continue until you fix this problem." + ).arg(dataPath) + ); + return; + } + if (!QDir::setCurrent(dataPath)) + { + showFatalErrorMessage( + "The launcher data folder could not be opened.", + QString( + "The launcher data folder could not be opened.\n" + "\n" +#if defined(Q_OS_MAC) + MACOS_HINT +#endif + "Make sure you have the right permissions to the launcher data folder.\n" + "(%1)\n" + "\n" + "The launcher cannot continue until you fix this problem." + ).arg(dataPath) + ); + return; + } + + // --world and --server can't be used together + if(!m_worldToJoin.isEmpty() && !m_serverToJoin.isEmpty()) + { + std::cerr << "--server and --world are mutually exclusive!" << std::endl; + m_status = Application::Failed; + return; + } + + // all the things invalid when NOT trying to --launch + if(m_instanceIdToLaunch.isEmpty()) { + if(!m_serverToJoin.isEmpty()) + { + std::cerr << "--server can only be used in combination with --launch!" << std::endl; + m_status = Application::Failed; + return; + } + + if(!m_worldToJoin.isEmpty()) + { + std::cerr << "--world can only be used in combination with --launch!" << std::endl; + m_status = Application::Failed; + return; + } + + if(!m_profileToUse.isEmpty()) + { + std::cerr << "--account can only be used in combination with --launch!" << std::endl; + m_status = Application::Failed; + return; + } + + if(m_offline) + { + std::cerr << "--offline can only be used in combination with --launch!" << std::endl; + m_status = Application::Failed; + return; + } + + if(!m_offlineName.isEmpty()) + { + std::cerr << "--offlineName can only be used in combination with --launch and --offline!" << std::endl; + m_status = Application::Failed; + return; + } + } + else { + // all the things invalid when trying to --launch + // online, and offline name is set + if(!m_offline && !m_offlineName.isEmpty()) { + std::cerr << "--offlineName can only be used in combination with --launch and --offline!" << std::endl; + m_status = Application::Failed; + return; + } + } + +#if defined(Q_OS_MAC) + // move user data to new location if on macOS and it still exists in Contents/MacOS + QDir fi(applicationDirPath()); + QString originalData = fi.absolutePath(); + // if the config file exists in Contents/MacOS, then user data is still there and needs to moved + if (QFileInfo::exists(FS::PathCombine(originalData, BuildConfig.LAUNCHER_CONFIGFILE))) + { + if (!QFileInfo::exists(FS::PathCombine(originalData, "dontmovemacdata"))) + { + QMessageBox::StandardButton askMoveDialogue; + askMoveDialogue = QMessageBox::question( + nullptr, + BuildConfig.LAUNCHER_DISPLAYNAME, + "Would you like to move application data to a new data location? It will improve the launcher's performance, but if you switch to older versions it will look like instances have disappeared. If you select no, you can migrate later in settings. You should select yes unless you're commonly switching between different versions (eg. develop and stable).", + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes + ); + if (askMoveDialogue == QMessageBox::Yes) + { + qDebug() << "On macOS and found config file in old location, moving user data..."; + QDir dir; + QStringList dataFiles { + "*.log", // Launcher log files: ${Launcher_Name}-@.log + "accounts.json", + "accounts", + "assets", + "cache", + "icons", + "instances", + "libraries", + "meta", + "metacache", + "mods", + BuildConfig.LAUNCHER_CONFIGFILE, + "themes", + "translations" + }; + QDirIterator files(originalData, dataFiles); + while (files.hasNext()) { + QString filePath(files.next()); + QString fileName(files.fileName()); + if (!dir.rename(filePath, FS::PathCombine(dataPath, fileName))) + { + qWarning() << "Failed to move " << fileName; + } + } + } + else + { + dataPath = originalData; + QDir::setCurrent(dataPath); + QFile file(originalData + "/dontmovemacdata"); + file.open(QIODevice::WriteOnly); + } + } + else + { + dataPath = originalData; + QDir::setCurrent(dataPath); + } + } +#endif + + /* + * Establish the mechanism for communication with an already running MultiMC that uses the same data path. + * If there is one, tell it what the user actually wanted to do and exit. + * We want to initialize this before logging to avoid messing with the log of a potential already running copy. + */ + auto appID = ApplicationId::fromPathAndVersion(QDir::currentPath(), BuildConfig.printableVersionString()); + { + // FIXME: you can run the same binaries with multiple data dirs and they won't clash. This could cause issues for updates. + m_peerInstance = new LocalPeer(this, appID); + connect(m_peerInstance, &LocalPeer::messageReceived, this, &Application::messageReceived); + if(m_peerInstance->isClient()) { + int timeout = 2000; + + if(m_instanceIdToLaunch.isEmpty()) + { + ApplicationMessage activate; + activate.command = "activate"; + m_peerInstance->sendMessage(activate.serialize(), timeout); + + if(!m_zipToImport.isEmpty()) + { + ApplicationMessage import; + import.command = "import"; + import.args.insert("path", m_zipToImport.toString()); + m_peerInstance->sendMessage(import.serialize(), timeout); + } + } + else + { + ApplicationMessage launch; + launch.command = "launch"; + launch.args["id"] = m_instanceIdToLaunch; + + if(!m_serverToJoin.isEmpty()) + { + launch.args["server"] = m_serverToJoin; + } + if(!m_worldToJoin.isEmpty()) + { + launch.args["world"] = m_worldToJoin; + } + if(!m_profileToUse.isEmpty()) + { + launch.args["profile"] = m_profileToUse; + } + if(m_offline) { + launch.args["offline_enabled"] = "true"; + launch.args["offline_name"] = m_offlineName; + } + m_peerInstance->sendMessage(launch.serialize(), timeout); + } + m_status = Application::Succeeded; + return; + } + } + + // init the logger + { + static const QString logBase = BuildConfig.LAUNCHER_NAME + "-%0.log"; + auto moveFile = [](const QString &oldName, const QString &newName) + { + QFile::remove(newName); + QFile::copy(oldName, newName); + QFile::remove(oldName); + }; + + moveFile(logBase.arg(3), logBase.arg(4)); + moveFile(logBase.arg(2), logBase.arg(3)); + moveFile(logBase.arg(1), logBase.arg(2)); + moveFile(logBase.arg(0), logBase.arg(1)); + + logFile = std::unique_ptr(new QFile(logBase.arg(0))); + if(!logFile->open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) + { + showFatalErrorMessage( + "The launcher data folder is not writable!", + QString( + "The launcher couldn't create a log file - the data folder is not writable.\n" + "\n" + #if defined(Q_OS_MAC) + MACOS_HINT + #endif + "Make sure you have write permissions to the data folder.\n" + "(%1)\n" + "\n" + "The launcher cannot continue until you fix this problem." + ).arg(dataPath) + ); + return; + } + qInstallMessageHandler(appDebugOutput); + qDebug() << "<> Log initialized."; + } + + // Set up paths + { + // Root path is used for updates. +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + QDir foo(FS::PathCombine(binPath, "..")); + m_rootPath = foo.absolutePath(); +#elif defined(Q_OS_WIN32) + m_rootPath = binPath; +#elif defined(Q_OS_MAC) + QDir foo(FS::PathCombine(binPath, "../..")); + m_rootPath = foo.absolutePath(); + // on macOS, touch the root to force Finder to reload the .app metadata (and fix any icon change issues) + FS::updateTimestamp(m_rootPath); +#endif + + qDebug() << BuildConfig.LAUNCHER_DISPLAYNAME << ", (c) 2013-2023 " << BuildConfig.LAUNCHER_COPYRIGHT; + qDebug() << "Version : " << BuildConfig.printableVersionString(); + qDebug() << "Git commit : " << BuildConfig.GIT_COMMIT; + qDebug() << "Git refspec : " << BuildConfig.GIT_REFSPEC; + if (adjustedBy.size()) + { + qDebug() << "Work dir before adjustment : " << origcwdPath; + qDebug() << "Work dir after adjustment : " << QDir::currentPath(); + qDebug() << "Adjusted by : " << adjustedBy; + } + else + { + qDebug() << "Work dir : " << QDir::currentPath(); + } + qDebug() << "Binary path : " << binPath; + qDebug() << "Application root path : " << m_rootPath; + if(!m_instanceIdToLaunch.isEmpty()) + { + qDebug() << "ID of instance to launch : " << m_instanceIdToLaunch; + } + if(!m_serverToJoin.isEmpty()) + { + qDebug() << "Address of server to join :" << m_serverToJoin; + } + if(!m_worldToJoin.isEmpty()) + { + qDebug() << "Name of world to join :" << m_worldToJoin; + } + qDebug() << "<> Paths set."; + } + + do // once + { + if(m_liveCheck) + { + QFile check(liveCheckFile); + if(!check.open(QIODevice::WriteOnly | QIODevice::Truncate)) + { + qWarning() << "Could not open" << liveCheckFile << "for writing!"; + break; + } + auto payload = appID.toString().toUtf8(); + if(check.write(payload) != payload.size()) + { + qWarning() << "Could not write into" << liveCheckFile << "!"; + check.remove(); + break; + } + check.close(); + } + } while(false); + + // Initialize application settings + { + m_settings.reset(new INISettingsObject(BuildConfig.LAUNCHER_CONFIGFILE, this)); + // Updates + m_settings->registerSetting("AutoUpdate", true); + + // Theming + m_settings->registerSetting("IconTheme", QString("multimc")); + m_settings->registerSetting("ApplicationTheme", QString("system")); + + // Notifications + m_settings->registerSetting("ShownNotifications", QString()); + + // Remembered state + m_settings->registerSetting("LastUsedGroupForNewInstance", QString()); + + QString defaultMonospace; + int defaultSize = 11; +#ifdef Q_OS_WIN32 + defaultMonospace = "Courier"; + defaultSize = 10; +#elif defined(Q_OS_MAC) + defaultMonospace = "Menlo"; +#else + defaultMonospace = "Monospace"; +#endif + + // resolve the font so the default actually matches + QFont consoleFont; + consoleFont.setFamily(defaultMonospace); + consoleFont.setStyleHint(QFont::Monospace); + consoleFont.setFixedPitch(true); + QFontInfo consoleFontInfo(consoleFont); + QString resolvedDefaultMonospace = consoleFontInfo.family(); + QFont resolvedFont(resolvedDefaultMonospace); + qDebug() << "Detected default console font:" << resolvedDefaultMonospace + << ", substitutions:" << resolvedFont.substitutions().join(','); + + m_settings->registerSetting("ConsoleFont", resolvedDefaultMonospace); + m_settings->registerSetting("ConsoleFontSize", defaultSize); + m_settings->registerSetting("ConsoleMaxLines", 100000); + m_settings->registerSetting("ConsoleOverflowStop", true); + + // Folders + m_settings->registerSetting("InstanceDir", "instances"); + m_settings->registerSetting({"CentralModsDir", "ModsDir"}, "mods"); + m_settings->registerSetting("IconsDir", "icons"); + + // Editors + m_settings->registerSetting("JsonEditor", QString()); + + // Language + m_settings->registerSetting("Language", QString()); + + // Console + m_settings->registerSetting("ShowConsole", false); + m_settings->registerSetting("AutoCloseConsole", false); + m_settings->registerSetting("ShowConsoleOnError", true); + m_settings->registerSetting("LogPrePostOutput", true); + + // Window Size + m_settings->registerSetting({"LaunchMaximized", "MCWindowMaximize"}, false); + m_settings->registerSetting({"MinecraftWinWidth", "MCWindowWidth"}, 854); + m_settings->registerSetting({"MinecraftWinHeight", "MCWindowHeight"}, 480); + + // Proxy Settings + m_settings->registerSetting("ProxyType", "None"); + m_settings->registerSetting({"ProxyAddr", "ProxyHostName"}, "127.0.0.1"); + m_settings->registerSetting("ProxyPort", 8080); + m_settings->registerSetting({"ProxyUser", "ProxyUsername"}, ""); + m_settings->registerSetting({"ProxyPass", "ProxyPassword"}, ""); + + // Memory + m_settings->registerSetting({"MinMemAlloc", "MinMemoryAlloc"}, 512); + m_settings->registerSetting({"MaxMemAlloc", "MaxMemoryAlloc"}, 1024); + m_settings->registerSetting("PermGen", 128); + + // Java Settings + m_settings->registerSetting("JavaPath", ""); + m_settings->registerSetting("JavaTimestamp", 0); + m_settings->registerSetting("JavaArchitecture", ""); + m_settings->registerSetting("JavaVersion", ""); + m_settings->registerSetting("JavaVendor", ""); + m_settings->registerSetting("LastHostname", ""); + m_settings->registerSetting("JvmArgs", ""); + + // Native library workarounds + m_settings->registerSetting("UseNativeOpenAL", false); + m_settings->registerSetting("UseNativeGLFW", false); + + // Game time + m_settings->registerSetting("ShowGameTime", true); + m_settings->registerSetting("ShowGlobalGameTime", true); + m_settings->registerSetting("RecordGameTime", true); + m_settings->registerSetting("ShowGameTimeHours", false); + + // Minecraft launch method + m_settings->registerSetting("MCLaunchMethod", "LauncherPart"); + + // Minecraft offline player name + m_settings->registerSetting("LastOfflinePlayerName", ""); + + // Wrapper command for launch + m_settings->registerSetting("WrapperCommand", ""); + + // Custom Commands + m_settings->registerSetting({"PreLaunchCommand", "PreLaunchCmd"}, ""); + m_settings->registerSetting({"PostExitCommand", "PostExitCmd"}, ""); + + // The cat + m_settings->registerSetting("TheCat", false); + + m_settings->registerSetting("InstSortMode", "Name"); + m_settings->registerSetting("SelectedInstance", QString()); + + // Window state and geometry + m_settings->registerSetting("MainWindowState", ""); + m_settings->registerSetting("MainWindowGeometry", ""); + + m_settings->registerSetting("ConsoleWindowState", ""); + m_settings->registerSetting("ConsoleWindowGeometry", ""); + + m_settings->registerSetting("SettingsGeometry", ""); + + m_settings->registerSetting("PagedGeometry", ""); + + m_settings->registerSetting("NewInstanceGeometry", ""); + + m_settings->registerSetting("UpdateDialogGeometry", ""); + + // paste.ee API key + m_settings->registerSetting("PasteEEAPIKey", "multimc"); + + if(!BuildConfig.ANALYTICS_ID.isEmpty()) + { + // Analytics + m_settings->registerSetting("Analytics", true); + m_settings->registerSetting("AnalyticsSeen", 0); + m_settings->registerSetting("AnalyticsClientID", QString()); + } + + // Init page provider + { + m_globalSettingsProvider = std::make_shared(tr("Settings")); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + m_globalSettingsProvider->addPage(); + } + qDebug() << "<> Settings loaded."; + } + +#ifndef QT_NO_ACCESSIBILITY + QAccessible::installFactory(groupViewAccessibleFactory); +#endif /* !QT_NO_ACCESSIBILITY */ + + // initialize network access and proxy setup + { + m_network = new QNetworkAccessManager(); + QString proxyTypeStr = settings()->get("ProxyType").toString(); + QString addr = settings()->get("ProxyAddr").toString(); + int port = settings()->get("ProxyPort").value(); + QString user = settings()->get("ProxyUser").toString(); + QString pass = settings()->get("ProxyPass").toString(); + updateProxySettings(proxyTypeStr, addr, port, user, pass); + qDebug() << "<> Network done."; + } + + // load translations + { + m_translations.reset(new TranslationsModel("translations")); + auto bcp47Name = m_settings->get("Language").toString(); + m_translations->selectLanguage(bcp47Name); + qDebug() << "Your language is" << bcp47Name; + qDebug() << "<> Translations loaded."; + } + + // initialize the updater + if(BuildConfig.UPDATER_ENABLED) + { + auto platform = getIdealPlatform(BuildConfig.BUILD_PLATFORM); + auto channelUrl = BuildConfig.UPDATER_BASE + platform + "/channels.json"; + qDebug() << "Initializing updater with platform: " << platform << " -- " << channelUrl; + m_updateChecker.reset(new UpdateChecker(m_network, channelUrl, BuildConfig.VERSION_BUILD)); + qDebug() << "<> Updater started."; + } + + // Instance icons + { + auto setting = APPLICATION->settings()->getSetting("IconsDir"); + QStringList instFolders = + { + ":/icons/multimc/32x32/instances/", + ":/icons/multimc/50x50/instances/", + ":/icons/multimc/128x128/instances/", + ":/icons/multimc/scalable/instances/" + }; + m_icons.reset(new IconList(instFolders, setting->get().toString())); + connect(setting.get(), &Setting::SettingChanged,[&](const Setting &, QVariant value) + { + m_icons->directoryChanged(value.toString()); + }); + qDebug() << "<> Instance icons intialized."; + } + + // Icon themes + { + // TODO: icon themes and instance icons do not mesh well together. Rearrange and fix discrepancies! + // set icon theme search path! + auto searchPaths = QIcon::themeSearchPaths(); + searchPaths.append("iconthemes"); + QIcon::setThemeSearchPaths(searchPaths); + qDebug() << "<> Icon themes initialized."; + } + + // Initialize widget themes + { + auto insertTheme = [this](ITheme * theme) + { + m_themes.insert(std::make_pair(theme->id(), std::unique_ptr(theme))); + }; + auto darkTheme = new DarkTheme(); + insertTheme(new SystemTheme()); + insertTheme(darkTheme); + insertTheme(new BrightTheme()); + insertTheme(new CustomTheme(darkTheme, "custom")); + qDebug() << "<> Widget themes initialized."; + } + + // initialize and load all instances + { + auto InstDirSetting = m_settings->getSetting("InstanceDir"); + // instance path: check for problems with '!' in instance path and warn the user in the log + // and remember that we have to show him a dialog when the gui starts (if it does so) + QString instDir = InstDirSetting->get().toString(); + qDebug() << "Instance path : " << instDir; + if (FS::checkProblemticPathJava(QDir(instDir))) + { + qWarning() << "Your instance path contains \'!\' and this is known to cause java problems!"; + } + m_instances.reset(new InstanceList(m_settings, instDir, this)); + connect(InstDirSetting.get(), &Setting::SettingChanged, m_instances.get(), &InstanceList::on_InstFolderChanged); + qDebug() << "Loading Instances..."; + m_instances->loadList(); + qDebug() << "<> Instances loaded."; + } + + { + m_authserver.reset(new AuthServer(this)); + qDebug() << "<> Auth server started."; + } + + // load auth providers + { + AuthProviders::load(m_authserver); + } + + // and accounts + { + m_accounts.reset(new AccountList(this)); + qDebug() << "Loading accounts..."; + m_accounts->setListFilePath("accounts.json", true); + m_accounts->loadList(); + m_accounts->fillQueue(); + qDebug() << "<> Accounts loaded."; + } + + // init the http meta cache + { + m_metacache.reset(new HttpMetaCache("metacache")); + m_metacache->addBase("asset_indexes", QDir("assets/indexes").absolutePath()); + m_metacache->addBase("asset_objects", QDir("assets/objects").absolutePath()); + m_metacache->addBase("versions", QDir("versions").absolutePath()); + m_metacache->addBase("libraries", QDir("libraries").absolutePath()); + m_metacache->addBase("minecraftforge", QDir("mods/minecraftforge").absolutePath()); + m_metacache->addBase("fmllibs", QDir("mods/minecraftforge/libs").absolutePath()); + m_metacache->addBase("liteloader", QDir("mods/liteloader").absolutePath()); + m_metacache->addBase("general", QDir("cache").absolutePath()); + m_metacache->addBase("ATLauncherPacks", QDir("cache/ATLauncherPacks").absolutePath()); + m_metacache->addBase("FTBPacks", QDir("cache/FTBPacks").absolutePath()); + m_metacache->addBase("TechnicPacks", QDir("cache/TechnicPacks").absolutePath()); + m_metacache->addBase("ModrinthPacks", QDir("cache/ModrinthPacks").absolutePath()); + m_metacache->addBase("root", QDir::currentPath()); + m_metacache->addBase("translations", QDir("translations").absolutePath()); + m_metacache->addBase("icons", QDir("cache/icons").absolutePath()); + m_metacache->addBase("meta", QDir("meta").absolutePath()); + m_metacache->addBase("injectors", QDir("injectors").absolutePath()); + m_metacache->Load(); + qDebug() << "<> Cache initialized."; + } + + // now we have network, download translation updates + m_translations->downloadIndex(); + + //FIXME: what to do with these? + m_profilers.insert("jprofiler", std::shared_ptr(new JProfilerFactory())); + m_profilers.insert("jvisualvm", std::shared_ptr(new JVisualVMFactory())); + for (auto profiler : m_profilers.values()) + { + profiler->registerSettings(m_settings); + } + + // Create the MCEdit thing... why is this here? + { + m_mcedit.reset(new MCEditTool(m_settings)); + } + + connect(this, &Application::aboutToQuit, [this](){ + if(m_instances) + { + // save any remaining instance state + m_instances->saveNow(); + } + if(logFile) + { + logFile->flush(); + logFile->close(); + } + }); + + { + setIconTheme(settings()->get("IconTheme").toString()); + qDebug() << "<> Icon theme set."; + setApplicationTheme(settings()->get("ApplicationTheme").toString(), true); + qDebug() << "<> Application theme set."; + } + + // Initialize analytics + /* + [this]() + { + const int analyticsVersion = 2; + if(BuildConfig.ANALYTICS_ID.isEmpty()) + { + return; + } + + auto analyticsSetting = m_settings->getSetting("Analytics"); + connect(analyticsSetting.get(), &Setting::SettingChanged, this, &Application::analyticsSettingChanged); + QString clientID = m_settings->get("AnalyticsClientID").toString(); + if(clientID.isEmpty()) + { + clientID = QUuid::createUuid().toString(); + clientID.remove(QLatin1Char('{')); + clientID.remove(QLatin1Char('}')); + m_settings->set("AnalyticsClientID", clientID); + } + m_analytics = new GAnalytics(BuildConfig.ANALYTICS_ID, clientID, analyticsVersion, this); + m_analytics->setLogLevel(GAnalytics::Debug); + m_analytics->setAnonymizeIPs(true); + // FIXME: the ganalytics library has no idea about our fancy shared pointers... + m_analytics->setNetworkAccessManager(network().get()); + + if(m_settings->get("AnalyticsSeen").toInt() < m_analytics->version()) + { + qDebug() << "Analytics info not seen by user yet (or old version)."; + return; + } + if(!m_settings->get("Analytics").toBool()) + { + qDebug() << "Analytics disabled by user."; + return; + } + + m_analytics->enable(); + qDebug() << "<> Initialized analytics with tid" << BuildConfig.ANALYTICS_ID; + }(); + */ + + if(createSetupWizard()) + { + return; + } + performMainStartupAction(); +} + +bool Application::createSetupWizard() +{ + bool javaRequired = [&]() + { + QString currentHostName = QHostInfo::localHostName(); + QString oldHostName = settings()->get("LastHostname").toString(); + if (currentHostName != oldHostName) + { + settings()->set("LastHostname", currentHostName); + return true; + } + QString currentJavaPath = settings()->get("JavaPath").toString(); + QString actualPath = FS::ResolveExecutable(currentJavaPath); + if (actualPath.isNull()) + { + return true; + } + return false; + }(); + bool analyticsRequired = [&]() + { + if(!m_analytics) { + return false; + } + if(BuildConfig.ANALYTICS_ID.isEmpty()) { + return false; + } + if (!settings()->get("Analytics").toBool()) { + return false; + } + if (settings()->get("AnalyticsSeen").toInt() < analytics()->version()) { + return true; + } + return false; + }(); + bool languageRequired = [&]() + { + if (settings()->get("Language").toString().isEmpty()) + return true; + return false; + }(); + bool wizardRequired = javaRequired || analyticsRequired || languageRequired; + + if(wizardRequired) + { + m_setupWizard = new SetupWizard(nullptr); + if (languageRequired) + { + m_setupWizard->addPage(new LanguageWizardPage(m_setupWizard)); + } + if (javaRequired) + { + m_setupWizard->addPage(new JavaWizardPage(m_setupWizard)); + } + if(analyticsRequired) + { + m_setupWizard->addPage(new AnalyticsWizardPage(m_setupWizard)); + } + connect(m_setupWizard, &QDialog::finished, this, &Application::setupWizardFinished); + m_setupWizard->show(); + return true; + } + return false; +} + +void Application::setupWizardFinished(int status) +{ + qDebug() << "Wizard result =" << status; + performMainStartupAction(); +} + +void Application::performMainStartupAction() +{ + m_status = Application::Initialized; + if(!m_instanceIdToLaunch.isEmpty()) + { + auto inst = instances()->getInstanceById(m_instanceIdToLaunch); + if(inst) + { + QuickPlayTargetPtr serverOrWorldToJoin = nullptr; + MinecraftAccountPtr accountToUse = nullptr; + bool offline = m_offline; + + qDebug() << "<> Instance" << m_instanceIdToLaunch << "launching"; + if(!m_serverToJoin.isEmpty()) + { + // FIXME: validate the server string + serverOrWorldToJoin.reset(new QuickPlayTarget(QuickPlayTarget::parseMultiplayer(m_serverToJoin))); + qDebug() << " Launching with server" << m_serverToJoin; + } + + if(!m_worldToJoin.isEmpty()) + { + serverOrWorldToJoin.reset(new QuickPlayTarget(QuickPlayTarget::parseSingleplayer(m_worldToJoin))); + qDebug() << " Launching with world" << m_worldToJoin; + } + + if(!m_profileToUse.isEmpty()) + { + accountToUse = accounts()->getAccountByProfileName(m_profileToUse); + if(!accountToUse) { + return; + } + qDebug() << " Launching with account" << m_profileToUse; + } + + launch(inst, !offline, nullptr, serverOrWorldToJoin, accountToUse, m_offlineName); + return; + } + } + if(!m_mainWindow) + { + // normal main window + showMainWindow(false); + qDebug() << "<> Main window shown."; + } + if(!m_zipToImport.isEmpty()) + { + qDebug() << "<> Importing instance from zip:" << m_zipToImport; + m_mainWindow->droppedURLs({ m_zipToImport }); + } +} + +void Application::showFatalErrorMessage(const QString& title, const QString& content) +{ + m_status = Application::Failed; + auto dialog = CustomMessageBox::selectable(nullptr, title, content, QMessageBox::Critical); + dialog->exec(); +} + +Application::~Application() +{ + // Shut down logger by setting the logger function to nothing + qInstallMessageHandler(nullptr); + +#if defined Q_OS_WIN32 + // Detach from Windows console + if(consoleAttached) + { + fclose(stdout); + fclose(stdin); + fclose(stderr); + FreeConsole(); + } +#endif +} + +void Application::messageReceived(const QByteArray& message) +{ + if(status() != Initialized) + { + qDebug() << "Received message" << message << "while still initializing. It will be ignored."; + return; + } + + ApplicationMessage received; + received.parse(message); + + auto & command = received.command; + + if(command == "activate") + { + showMainWindow(); + } + else if(command == "import") + { + QString path = received.args["path"]; + if(path.isEmpty()) + { + qWarning() << "Received" << command << "message without a zip path/URL."; + return; + } + m_mainWindow->droppedURLs({ QUrl(path) }); + } + else if(command == "launch") + { + QString id = received.args["id"]; + QString server = received.args["server"]; + QString world = received.args["world"]; + QString profile = received.args["profile"]; + bool offline = received.args["offline_enabled"] == "true"; + QString offlineName = received.args["offline_name"]; + + InstancePtr instance; + if(!id.isEmpty()) { + instance = instances()->getInstanceById(id); + if(!instance) { + qWarning() << "Launch command requires an valid instance ID. " << id << "resolves to nothing."; + return; + } + } + else { + qWarning() << "Launch command called without an instance ID..."; + return; + } + + QuickPlayTargetPtr quickPlayTarget = nullptr; + if(!server.isEmpty()) { + quickPlayTarget = std::make_shared(QuickPlayTarget::parseMultiplayer(server)); + } else if(!world.isEmpty()) { + quickPlayTarget = std::make_shared(QuickPlayTarget::parseSingleplayer(world)); + } + + MinecraftAccountPtr accountObject; + if(!profile.isEmpty()) { + accountObject = accounts()->getAccountByProfileName(profile); + if(!accountObject) { + qWarning() << "Launch command requires the specified profile to be valid. " << profile << "does not resolve to any account."; + return; + } + } + + launch( + instance, + !offline, + nullptr, + quickPlayTarget, + accountObject, + offlineName + ); + } + else + { + qWarning() << "Received invalid message" << message; + } +} + +void Application::analyticsSettingChanged(const Setting&, QVariant value) +{ + if(!m_analytics) + return; + bool enabled = value.toBool(); + if(enabled) + { + qDebug() << "Analytics enabled by user."; + } + else + { + qDebug() << "Analytics disabled by user."; + } + m_analytics->enable(enabled); +} + +std::shared_ptr Application::translations() +{ + return m_translations; +} + +std::shared_ptr Application::javalist() +{ + if (!m_javalist) + { + m_javalist.reset(new JavaInstallList()); + } + return m_javalist; +} + +std::vector Application::getValidApplicationThemes() +{ + std::vector ret; + auto iter = m_themes.cbegin(); + while (iter != m_themes.cend()) + { + ret.push_back((*iter).second.get()); + iter++; + } + return ret; +} + +void Application::setApplicationTheme(const QString& name, bool initial) +{ + auto systemPalette = qApp->palette(); + auto themeIter = m_themes.find(name); + if(themeIter != m_themes.end()) + { + auto & theme = (*themeIter).second; + theme->apply(initial); + } + else + { + qWarning() << "Tried to set invalid theme:" << name; + } +} + +void Application::setIconTheme(const QString& name) +{ + XdgIcon::setThemeName(name); +} + +QIcon Application::getThemedIcon(const QString& name) +{ + if(name == "logo") { + return QIcon(":/logo.svg"); + } + return XdgIcon::fromTheme(name); +} + +bool Application::openJsonEditor(const QString &filename) +{ + const QString file = QDir::current().absoluteFilePath(filename); + if (m_settings->get("JsonEditor").toString().isEmpty()) + { + return DesktopServices::openUrl(QUrl::fromLocalFile(file)); + } + else + { + //return DesktopServices::openFile(m_settings->get("JsonEditor").toString(), file); + return DesktopServices::run(m_settings->get("JsonEditor").toString(), {file}); + } +} + +bool Application::launch( + InstancePtr instance, + bool online, + BaseProfilerFactory *profiler, + QuickPlayTargetPtr quickPlayTarget, + MinecraftAccountPtr accountToUse, + const QString& offlineName +) { + if(m_updateRunning) + { + qDebug() << "Cannot launch instances while an update is running. Please try again when updates are completed."; + } + else if(instance->canLaunch()) + { + auto & extras = m_instanceExtras[instance->id()]; + auto & window = extras.window; + if(window) + { + if(!window->saveAll()) + { + return false; + } + } + auto & controller = extras.controller; + controller.reset(new LaunchController()); + controller->setInstance(instance); + controller->setOnline(online); + controller->setProfiler(profiler); + controller->setQuickPlayTarget(quickPlayTarget); + controller->setAuthserver(m_authserver); + controller->setAccountToUse(accountToUse); + controller->setOfflineName(offlineName); + if(window) + { + controller->setParentWidget(window); + } + else if(m_mainWindow) + { + controller->setParentWidget(m_mainWindow); + } + connect(controller.get(), &LaunchController::succeeded, this, &Application::controllerSucceeded); + connect(controller.get(), &LaunchController::failed, this, &Application::controllerFailed); + addRunningInstance(); + controller->start(); + return true; + } + else if (instance->isRunning()) + { + showInstanceWindow(instance, "console"); + return true; + } + else if (instance->canEdit()) + { + showInstanceWindow(instance); + return true; + } + return false; +} + +bool Application::kill(InstancePtr instance) +{ + if (!instance->isRunning()) + { + qWarning() << "Attempted to kill instance" << instance->id() << ", which isn't running."; + return false; + } + auto & extras = m_instanceExtras[instance->id()]; + // NOTE: copy of the shared pointer keeps it alive + auto controller = extras.controller; + if(controller) + { + return controller->abort(); + } + return true; +} + +void Application::addRunningInstance() +{ + m_runningInstances ++; + if(m_runningInstances == 1) + { + emit updateAllowedChanged(false); + } +} + +void Application::subRunningInstance() +{ + if(m_runningInstances == 0) + { + qCritical() << "Something went really wrong and we now have less than 0 running instances... WTF"; + return; + } + m_runningInstances --; + if(m_runningInstances == 0) + { + emit updateAllowedChanged(true); + } +} + +bool Application::shouldExitNow() const +{ + return m_runningInstances == 0 && m_openWindows == 0; +} + +bool Application::updatesAreAllowed() +{ + return m_runningInstances == 0; +} + +void Application::updateIsRunning(bool running) +{ + m_updateRunning = running; +} + + +void Application::controllerSucceeded() +{ + auto controller = qobject_cast(QObject::sender()); + if(!controller) + return; + auto id = controller->id(); + auto & extras = m_instanceExtras[id]; + + // on success, do... + if (controller->instance()->settings()->get("AutoCloseConsole").toBool()) + { + if(extras.window) + { + extras.window->close(); + } + } + extras.controller.reset(); + subRunningInstance(); + + // quit when there are no more windows. + if(shouldExitNow()) + { + m_status = Status::Succeeded; + exit(0); + } +} + +void Application::controllerFailed(const QString& error) +{ + Q_UNUSED(error); + auto controller = qobject_cast(QObject::sender()); + if(!controller) + return; + auto id = controller->id(); + auto & extras = m_instanceExtras[id]; + + // on failure, do... nothing + extras.controller.reset(); + subRunningInstance(); + + // quit when there are no more windows. + if(shouldExitNow()) + { + m_status = Status::Failed; + exit(1); + } +} + +void Application::ShowGlobalSettings(class QWidget* parent, QString open_page) +{ + if(!m_globalSettingsProvider) { + return; + } + emit globalSettingsAboutToOpen(); + { + SettingsObject::Lock lock(APPLICATION->settings()); + PageDialog dlg(m_globalSettingsProvider.get(), open_page, parent); + dlg.exec(); + } + emit globalSettingsClosed(); +} + +MainWindow* Application::showMainWindow(bool minimized) +{ + if(m_mainWindow) + { + m_mainWindow->setWindowState(m_mainWindow->windowState() & ~Qt::WindowMinimized); + m_mainWindow->raise(); + m_mainWindow->activateWindow(); + } + else + { + m_mainWindow = new MainWindow(); + m_mainWindow->restoreState(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowState").toByteArray())); + m_mainWindow->restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("MainWindowGeometry").toByteArray())); + if(minimized) + { + m_mainWindow->showMinimized(); + } + else + { + m_mainWindow->show(); + } + + m_mainWindow->checkInstancePathForProblems(); + connect(this, &Application::updateAllowedChanged, m_mainWindow, &MainWindow::updatesAllowedChanged); + connect(m_mainWindow, &MainWindow::isClosing, this, &Application::on_windowClose); + m_openWindows++; + } + // FIXME: move this somewhere else... + if(m_analytics) + { + auto windowSize = m_mainWindow->size(); + auto sizeString = QString("%1x%2").arg(windowSize.width()).arg(windowSize.height()); + qDebug() << "Viewport size" << sizeString; + m_analytics->setViewportSize(sizeString); + /* + * cm1 = java min heap [MB] + * cm2 = java max heap [MB] + * cm3 = system RAM [MB] + * + * cd1 = java version + * cd2 = java architecture + * cd3 = system architecture + * cd4 = CPU architecture + */ + QVariantMap customValues; + int min = m_settings->get("MinMemAlloc").toInt(); + int max = m_settings->get("MaxMemAlloc").toInt(); + if(min < max) + { + customValues["cm1"] = min; + customValues["cm2"] = max; + } + else + { + customValues["cm1"] = max; + customValues["cm2"] = min; + } + + constexpr uint64_t Mega = 1024ull * 1024ull; + int ramSize = int(Sys::getSystemRam() / Mega); + qDebug() << "RAM size is" << ramSize << "MB"; + customValues["cm3"] = ramSize; + + customValues["cd1"] = m_settings->get("JavaVersion"); + customValues["cd2"] = m_settings->get("JavaArchitecture"); + customValues["cd3"] = Sys::isSystem64bit() ? "64":"32"; + customValues["cd4"] = Sys::isCPU64bit() ? "64":"32"; + auto kernelInfo = Sys::getKernelInfo(); + customValues["cd5"] = kernelInfo.kernelName; + customValues["cd6"] = kernelInfo.kernelVersion; + auto distInfo = Sys::getDistributionInfo(); + if(!distInfo.distributionName.isEmpty()) + { + customValues["cd7"] = distInfo.distributionName; + } + if(!distInfo.distributionVersion.isEmpty()) + { + customValues["cd8"] = distInfo.distributionVersion; + } + m_analytics->sendScreenView("Main Window", customValues); + } + return m_mainWindow; +} + +InstanceWindow *Application::showInstanceWindow(InstancePtr instance, QString page) +{ + if(!instance) + return nullptr; + auto id = instance->id(); + auto & extras = m_instanceExtras[id]; + auto & window = extras.window; + + if(window) + { + window->raise(); + window->activateWindow(); + } + else + { + window = new InstanceWindow(instance); + m_openWindows ++; + connect(window, &InstanceWindow::isClosing, this, &Application::on_windowClose); + } + if(!page.isEmpty()) + { + window->selectPage(page); + } + if(extras.controller) + { + extras.controller->setParentWidget(window); + } + return window; +} + +void Application::on_windowClose() +{ + m_openWindows--; + auto instWindow = qobject_cast(QObject::sender()); + if(instWindow) + { + auto & extras = m_instanceExtras[instWindow->instanceId()]; + extras.window = nullptr; + if(extras.controller) + { + extras.controller->setParentWidget(m_mainWindow); + } + } + auto mainWindow = qobject_cast(QObject::sender()); + if(mainWindow) + { + m_mainWindow = nullptr; + } + // quit when there are no more windows. + if(shouldExitNow()) + { + exit(0); + } +} + +QString Application::msaClientId() const { + return Secrets::getMSAClientID('-'); +} + +void Application::updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password) +{ + // Set the application proxy settings. + if (proxyTypeStr == "SOCKS5") + { + QNetworkProxy::setApplicationProxy( + QNetworkProxy(QNetworkProxy::Socks5Proxy, addr, port, user, password)); + } + else if (proxyTypeStr == "HTTP") + { + QNetworkProxy::setApplicationProxy( + QNetworkProxy(QNetworkProxy::HttpProxy, addr, port, user, password)); + } + else if (proxyTypeStr == "None") + { + // If we have no proxy set, set no proxy and return. + QNetworkProxy::setApplicationProxy(QNetworkProxy(QNetworkProxy::NoProxy)); + } + else + { + // If we have "Default" selected, set Qt to use the system proxy settings. + QNetworkProxyFactory::setUseSystemConfiguration(true); + } + + qDebug() << "Detecting proxy settings..."; + QNetworkProxy proxy = QNetworkProxy::applicationProxy(); + m_network->setProxy(proxy); + + QString proxyDesc; + if (proxy.type() == QNetworkProxy::NoProxy) + { + qDebug() << "Using no proxy is an option!"; + return; + } + switch (proxy.type()) + { + case QNetworkProxy::DefaultProxy: + proxyDesc = "Default proxy: "; + break; + case QNetworkProxy::Socks5Proxy: + proxyDesc = "Socks5 proxy: "; + break; + case QNetworkProxy::HttpProxy: + proxyDesc = "HTTP proxy: "; + break; + case QNetworkProxy::HttpCachingProxy: + proxyDesc = "HTTP caching: "; + break; + case QNetworkProxy::FtpCachingProxy: + proxyDesc = "FTP caching: "; + break; + default: + proxyDesc = "DERP proxy: "; + break; + } + proxyDesc += QString("%1:%2") + .arg(proxy.hostName()) + .arg(proxy.port()); + qDebug() << proxyDesc; +} + +shared_qobject_ptr< HttpMetaCache > Application::metacache() +{ + return m_metacache; +} + +shared_qobject_ptr Application::network() +{ + return m_network; +} + +shared_qobject_ptr Application::metadataIndex() +{ + if (!m_metadataIndex) + { + m_metadataIndex.reset(new Meta::Index()); + } + return m_metadataIndex; +} + +QString Application::getJarsPath() +{ + if(m_jarsPath.isEmpty()) + { + return FS::PathCombine(QCoreApplication::applicationDirPath(), "jars"); + } + return m_jarsPath; +} diff --git a/ultimmc/launcher/Application.h b/ultimmc/launcher/Application.h new file mode 100644 index 0000000..cb6bea8 --- /dev/null +++ b/ultimmc/launcher/Application.h @@ -0,0 +1,246 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "minecraft/launch/QuickPlayTarget.h" + +class LaunchController; +class LocalPeer; +class InstanceWindow; +class MainWindow; +class SetupWizard; +class GenericPageProvider; +class QFile; +class HttpMetaCache; +class SettingsObject; +class InstanceList; +class AccountList; +class IconList; +class QNetworkAccessManager; +class JavaInstallList; +class UpdateChecker; +class BaseProfilerFactory; +class BaseDetachedToolFactory; +class TranslationsModel; +class ITheme; +class MCEditTool; +class AuthServer; +class GAnalytics; + +namespace Meta { + class Index; +} + +#if defined(APPLICATION) +#undef APPLICATION +#endif +#define APPLICATION (static_cast(QCoreApplication::instance())) + +class Application : public QApplication +{ + // friends for the purpose of limiting access to deprecated stuff + Q_OBJECT +public: + enum Status { + StartingUp, + Failed, + Succeeded, + Initialized + }; + +public: + Application(int &argc, char **argv); + virtual ~Application(); + + GAnalytics *analytics() const { + return m_analytics; + } + + std::shared_ptr settings() const { + return m_settings; + } + + qint64 timeSinceStart() const { + return startTime.msecsTo(QDateTime::currentDateTime()); + } + + QIcon getThemedIcon(const QString& name); + + void setIconTheme(const QString& name); + + std::vector getValidApplicationThemes(); + + void setApplicationTheme(const QString& name, bool initial); + + shared_qobject_ptr updateChecker() { + return m_updateChecker; + } + + std::shared_ptr translations(); + + std::shared_ptr javalist(); + + std::shared_ptr instances() const { + return m_instances; + } + + std::shared_ptr icons() const { + return m_icons; + } + + MCEditTool *mcedit() const { + return m_mcedit.get(); + } + + shared_qobject_ptr accounts() const { + return m_accounts; + } + + QString msaClientId() const; + + Status status() const { + return m_status; + } + + const QMap> &profilers() const { + return m_profilers; + } + + void updateProxySettings(QString proxyTypeStr, QString addr, int port, QString user, QString password); + + shared_qobject_ptr network(); + + shared_qobject_ptr metacache(); + + shared_qobject_ptr metadataIndex(); + + QString getJarsPath(); + + /// this is the root of the 'installation'. Used for automatic updates + const QString &root() { + return m_rootPath; + } + + /*! + * Opens a json file using either a system default editor, or, if not empty, the editor + * specified in the settings + */ + bool openJsonEditor(const QString &filename); + + InstanceWindow *showInstanceWindow(InstancePtr instance, QString page = QString()); + MainWindow *showMainWindow(bool minimized = false); + + void updateIsRunning(bool running); + bool updatesAreAllowed(); + + void ShowGlobalSettings(class QWidget * parent, QString open_page = QString()); + +signals: + void updateAllowedChanged(bool status); + void globalSettingsAboutToOpen(); + void globalSettingsClosed(); + +public slots: + bool launch( + InstancePtr instance, + bool online = true, + BaseProfilerFactory *profiler = nullptr, + QuickPlayTargetPtr quickPlayTarget = nullptr, + MinecraftAccountPtr accountToUse = nullptr, + const QString &offlineName = QString() + ); + bool kill(InstancePtr instance); + +private slots: + void on_windowClose(); + void messageReceived(const QByteArray & message); + void controllerSucceeded(); + void controllerFailed(const QString & error); + void analyticsSettingChanged(const Setting &setting, QVariant value); + void setupWizardFinished(int status); + +private: + bool createSetupWizard(); + void performMainStartupAction(); + + // sets the fatal error message and m_status to Failed. + void showFatalErrorMessage(const QString & title, const QString & content); + +private: + void addRunningInstance(); + void subRunningInstance(); + bool shouldExitNow() const; + +private: + QDateTime startTime; + + shared_qobject_ptr m_network; + + shared_qobject_ptr m_updateChecker; + shared_qobject_ptr m_accounts; + + shared_qobject_ptr m_metacache; + shared_qobject_ptr m_metadataIndex; + + std::shared_ptr m_settings; + std::shared_ptr m_instances; + std::shared_ptr m_icons; + std::shared_ptr m_javalist; + std::shared_ptr m_translations; + std::shared_ptr m_globalSettingsProvider; + std::map> m_themes; + std::unique_ptr m_mcedit; + std::shared_ptr m_authserver; + QString m_jarsPath; + QSet m_features; + + QMap> m_profilers; + + QString m_rootPath; + Status m_status = Application::StartingUp; + +#if defined Q_OS_WIN32 + // used on Windows to attach the standard IO streams + bool consoleAttached = false; +#endif + + // FIXME: attach to instances instead. + struct InstanceXtras { + InstanceWindow * window = nullptr; + shared_qobject_ptr controller; + }; + std::map m_instanceExtras; + + // main state variables + size_t m_openWindows = 0; + size_t m_runningInstances = 0; + bool m_updateRunning = false; + + // main window, if any + MainWindow * m_mainWindow = nullptr; + + // peer launcher instance connector - used to implement single instance launcher and signalling + LocalPeer * m_peerInstance = nullptr; + + GAnalytics * m_analytics = nullptr; + SetupWizard * m_setupWizard = nullptr; +public: + QString m_instanceIdToLaunch; + QString m_serverToJoin; + QString m_worldToJoin; + QString m_profileToUse; + bool m_offline = false; + QString m_offlineName; + bool m_liveCheck = false; + QUrl m_zipToImport; + std::unique_ptr logFile; +}; diff --git a/ultimmc/launcher/ApplicationMessage.cpp b/ultimmc/launcher/ApplicationMessage.cpp new file mode 100644 index 0000000..e22bf13 --- /dev/null +++ b/ultimmc/launcher/ApplicationMessage.cpp @@ -0,0 +1,31 @@ +#include "ApplicationMessage.h" + +#include +#include + +void ApplicationMessage::parse(const QByteArray & input) { + auto doc = QJsonDocument::fromBinaryData(input); + auto root = doc.object(); + + command = root.value("command").toString(); + args.clear(); + + auto parsedArgs = root.value("args").toObject(); + for(auto iter = parsedArgs.begin(); iter != parsedArgs.end(); iter++) { + args[iter.key()] = iter.value().toString(); + } +} + +QByteArray ApplicationMessage::serialize() { + QJsonObject root; + root.insert("command", command); + QJsonObject outArgs; + for (auto iter = args.begin(); iter != args.end(); iter++) { + outArgs[iter.key()] = iter.value(); + } + root.insert("args", outArgs); + + QJsonDocument out; + out.setObject(root); + return out.toBinaryData(); +} diff --git a/ultimmc/launcher/ApplicationMessage.h b/ultimmc/launcher/ApplicationMessage.h new file mode 100644 index 0000000..745bdea --- /dev/null +++ b/ultimmc/launcher/ApplicationMessage.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include +#include + +struct ApplicationMessage { + QString command; + QMap args; + + QByteArray serialize(); + void parse(const QByteArray & input); +}; diff --git a/ultimmc/launcher/AuthServer.cpp b/ultimmc/launcher/AuthServer.cpp new file mode 100644 index 0000000..ebc0bb9 --- /dev/null +++ b/ultimmc/launcher/AuthServer.cpp @@ -0,0 +1,188 @@ +#include "AuthServer.h" + +#include +#include +#include +#include +#include + +struct Request +{ + QString method; + QString url; + QMap headers; + QByteArray body; + + QJsonDocument json() { + return QJsonDocument::fromJson(body.data()); + } +}; + +struct Response +{ + int statusCode; + QMap headers; + QString body; +}; + +enum ConnectionState +{ + CREATING, + READING_HEAD, + READING_BODY, + PROCESS_REQUEST +}; + +struct Connection +{ + ConnectionState state; + Request *request; + Response *response; + int leftToRead; + QByteArray buffer; +}; + +AuthServer::AuthServer(QObject *parent) : QObject(parent) +{ + m_tcpServer.reset(new QTcpServer(this)); + + connect(m_tcpServer.get(), &QTcpServer::newConnection, this, &AuthServer::newConnection); + + if (!m_tcpServer->listen(QHostAddress::LocalHost)) + { + // TODO: think about stop launching when server start fails + qCritical() << "Auth server start failed"; + } +} + +quint16 AuthServer::port() +{ + return m_tcpServer->serverPort(); +} + +void processRequest(Request *request, Response *response) +{ + qDebug() << "Processing request"; + if (request->url == "/") + { + response->body = "{\"Status\":\"OK\",\"Runtime-Mode\":\"productionMode\",\"Application-Author\":\"Mojang Web Force\",\"Application-Description\":\"Mojang Public API.\",\"Specification-Version\":\"6.0.0\",\"Application-Name\":\"yggdrasil.accounts.restlet.server.public\",\"Implementation-Version\":\"6.0.0\",\"Application-Owner\":\"Mojang\"}"; + response->statusCode = 200; + response->headers["Content-Type"] = "application/json; charset=utf-8"; + return; + } + + if (request->url == "/sessionserver/session/minecraft/join" || request->url == "/sessionserver/session/minecraft/hasJoined") + { + response->statusCode = 204; + return; + } + + if (request->url == "/auth/authenticate" || request->url == "/auth/refresh") + { + auto json = request->json().object(); + QString clientToken = json.value("clientToken").toString(); + QString username = json.value(request->url == "/auth/authenticate" ? "username" : "accessToken").toString(); + + QString profile = ((QString) "{\"id\":\"%1\",\"name\":\"%2\"}").arg(clientToken, username); + + response->statusCode = 200; + response->body = ((QString) "{\"accessToken\":\"%1\",\"clientToken\":\"%2\",\"availableProfiles\":[%3], \"selectedProfile\": %3}").arg(username, clientToken, profile); + return; + } + + response->body = "Not found"; + response->statusCode = 404; +} + +void AuthServer::newConnection() +{ + + QTcpSocket *tcpSocket = m_tcpServer->nextPendingConnection(); + Connection *connection = new Connection(); + + connect(tcpSocket, &QTcpSocket::readyRead, this, [tcpSocket, connection]() + { + // Not the best way to process queries, but it just works + QByteArray curBuf = tcpSocket->readAll().data(); + qDebug() << "Read " << curBuf.size() << " bytes"; + + if (connection->state == CREATING) + { + connection->response = new Response(); + connection->request = new Request(); + connection->buffer = ((QString)"").toUtf8(); + connection->state = READING_HEAD; + } + + if (connection->state == READING_HEAD) + { + connection->buffer.append(curBuf); + if (connection->buffer.contains("\r\n\r\n")) + { + QByteArray head = connection->buffer.left(connection->buffer.indexOf("\r\n\r\n")); + QString headStr = head.data(); + QStringList headList = headStr.split("\r\n"); + QStringList firstLine = headList.at(0).split(" "); + connection->request->method = firstLine.at(0); + connection->request->url = firstLine.at(1); + + for (int i = 1; i < headList.size(); i++) + { + QStringList header = headList.at(i).split(":"); + connection->request->headers.insert(header.at(0), header.at(1)); + } + + if (connection->request->headers.contains("Content-Length")) + { + connection->leftToRead = connection->request->headers["Content-Length"].toInt(); + } + else + { + connection->leftToRead = 0; + } + + curBuf = connection->buffer.mid(connection->buffer.indexOf("\r\n\r\n") + 4); + connection->state = READING_BODY; + } + } + + if (connection->state == READING_BODY) + { + connection->request->body.append(curBuf); + if (connection->request->body.size() >= connection->leftToRead) + { + connection->state = PROCESS_REQUEST; + } + } + + if(connection->state == PROCESS_REQUEST){ + processRequest(connection->request, connection->response); + + if(connection->response->body.size() > 0){ + connection->response->headers["Content-Length"] = QString::number(connection->response->body.size()); + } + connection->response->headers["Connection"] = "Keep-Alive"; + + QString responseStatusText = "Internal Server Error"; + if (connection->response->statusCode == 200) + responseStatusText = "OK"; + else if (connection->response->statusCode == 204) + responseStatusText = "No Content"; + else if (connection->response->statusCode == 404) + responseStatusText = "Not Found"; + + + QString responseHead = ((QString)"HTTP/1.1 %1 %2\r\n").arg(connection->response->statusCode).arg(responseStatusText); + for (auto h: connection->response->headers.keys()) + { + responseHead += ((QString)"%1: %2\r\n").arg(h, connection->response->headers[h]); + } + responseHead += "\r\n"; + tcpSocket->write(responseHead.toUtf8()); + tcpSocket->write(connection->response->body.toUtf8()); + connection->state = CREATING; + } + }); + connect(tcpSocket, &QTcpSocket::disconnected, this, [tcpSocket]() + { tcpSocket->close(); }); +} diff --git a/ultimmc/launcher/AuthServer.h b/ultimmc/launcher/AuthServer.h new file mode 100644 index 0000000..882586c --- /dev/null +++ b/ultimmc/launcher/AuthServer.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include "settings/SettingsObject.h" + +class AuthServer: public QObject +{ +public: + explicit AuthServer(QObject *parent = 0); + + quint16 port(); + +private: + void newConnection(); + +private: + std::shared_ptr m_tcpServer; +}; diff --git a/ultimmc/launcher/BaseInstaller.cpp b/ultimmc/launcher/BaseInstaller.cpp new file mode 100644 index 0000000..d61c3fe --- /dev/null +++ b/ultimmc/launcher/BaseInstaller.cpp @@ -0,0 +1,61 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "BaseInstaller.h" +#include "minecraft/MinecraftInstance.h" + +BaseInstaller::BaseInstaller() +{ + +} + +bool BaseInstaller::isApplied(MinecraftInstance *on) +{ + return QFile::exists(filename(on->instanceRoot())); +} + +bool BaseInstaller::add(MinecraftInstance *to) +{ + if (!patchesDir(to->instanceRoot()).exists()) + { + QDir(to->instanceRoot()).mkdir("patches"); + } + + if (isApplied(to)) + { + if (!remove(to)) + { + return false; + } + } + + return true; +} + +bool BaseInstaller::remove(MinecraftInstance *from) +{ + return QFile::remove(filename(from->instanceRoot())); +} + +QString BaseInstaller::filename(const QString &root) const +{ + return patchesDir(root).absoluteFilePath(id() + ".json"); +} +QDir BaseInstaller::patchesDir(const QString &root) const +{ + return QDir(root + "/patches/"); +} diff --git a/ultimmc/launcher/BaseInstaller.h b/ultimmc/launcher/BaseInstaller.h new file mode 100644 index 0000000..b2e6a14 --- /dev/null +++ b/ultimmc/launcher/BaseInstaller.h @@ -0,0 +1,44 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +class MinecraftInstance; +class QDir; +class QString; +class QObject; +class Task; +class BaseVersion; +typedef std::shared_ptr BaseVersionPtr; + +class BaseInstaller +{ +public: + BaseInstaller(); + virtual ~BaseInstaller(){}; + bool isApplied(MinecraftInstance *on); + + virtual bool add(MinecraftInstance *to); + virtual bool remove(MinecraftInstance *from); + + virtual Task *createInstallTask(MinecraftInstance *instance, BaseVersionPtr version, QObject *parent) = 0; + +protected: + virtual QString id() const = 0; + QString filename(const QString &root) const; + QDir patchesDir(const QString &root) const; +}; diff --git a/ultimmc/launcher/BaseInstance.cpp b/ultimmc/launcher/BaseInstance.cpp new file mode 100644 index 0000000..928a70a --- /dev/null +++ b/ultimmc/launcher/BaseInstance.cpp @@ -0,0 +1,330 @@ +/* Copyright 2013-2021 MultiMC Contributors + * Copyright 2022 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "BaseInstance.h" + +#include +#include +#include + +#include "settings/INISettingsObject.h" +#include "settings/Setting.h" +#include "settings/OverrideSetting.h" + +#include "FileSystem.h" +#include "Commandline.h" +#include "BuildConfig.h" + +BaseInstance::BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) + : QObject() +{ + m_settings = settings; + m_rootDir = rootDir; + + m_settings->registerSetting("name", "Unnamed Instance"); + m_settings->registerSetting("iconKey", "default"); + m_settings->registerSetting("notes", ""); + m_settings->registerSetting("lastLaunchTime", 0); + m_settings->registerSetting("totalTimePlayed", 0); + m_settings->registerSetting("lastTimePlayed", 0); + + // Custom Commands + auto commandSetting = m_settings->registerSetting({"OverrideCommands","OverrideLaunchCmd"}, false); + m_settings->registerOverride(globalSettings->getSetting("PreLaunchCommand"), commandSetting); + m_settings->registerOverride(globalSettings->getSetting("WrapperCommand"), commandSetting); + m_settings->registerOverride(globalSettings->getSetting("PostExitCommand"), commandSetting); + + // Console + auto consoleSetting = m_settings->registerSetting("OverrideConsole", false); + m_settings->registerOverride(globalSettings->getSetting("ShowConsole"), consoleSetting); + m_settings->registerOverride(globalSettings->getSetting("AutoCloseConsole"), consoleSetting); + m_settings->registerOverride(globalSettings->getSetting("ShowConsoleOnError"), consoleSetting); + m_settings->registerOverride(globalSettings->getSetting("LogPrePostOutput"), consoleSetting); + + m_settings->registerPassthrough(globalSettings->getSetting("ConsoleMaxLines"), nullptr); + m_settings->registerPassthrough(globalSettings->getSetting("ConsoleOverflowStop"), nullptr); + + // Managed Packs + m_settings->registerSetting("ManagedPack", false); + m_settings->registerSetting("ManagedPackType", ""); + m_settings->registerSetting("ManagedPackID", ""); + m_settings->registerSetting("ManagedPackName", ""); + m_settings->registerSetting("ManagedPackVersionID", ""); + m_settings->registerSetting("ManagedPackVersionName", ""); +} + +QString BaseInstance::getPreLaunchCommand() +{ + return settings()->get("PreLaunchCommand").toString(); +} + +QString BaseInstance::getWrapperCommand() +{ + return settings()->get("WrapperCommand").toString(); +} + +QString BaseInstance::getPostExitCommand() +{ + return settings()->get("PostExitCommand").toString(); +} + +bool BaseInstance::isManagedPack() +{ + return settings()->get("ManagedPack").toBool(); +} + +QString BaseInstance::getManagedPackType() +{ + return settings()->get("ManagedPackType").toString(); +} + +QString BaseInstance::getManagedPackID() +{ + return settings()->get("ManagedPackID").toString(); +} + +QString BaseInstance::getManagedPackName() +{ + return settings()->get("ManagedPackName").toString(); +} + +QString BaseInstance::getManagedPackVersionID() +{ + return settings()->get("ManagedPackVersionID").toString(); +} + +QString BaseInstance::getManagedPackVersionName() +{ + return settings()->get("ManagedPackVersionName").toString(); +} + +void BaseInstance::setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version) +{ + settings()->set("ManagedPack", true); + settings()->set("ManagedPackType", type); + settings()->set("ManagedPackID", id); + settings()->set("ManagedPackName", name); + settings()->set("ManagedPackVersionID", versionId); + settings()->set("ManagedPackVersionName", version); +} + +int BaseInstance::getConsoleMaxLines() const +{ + auto lineSetting = settings()->getSetting("ConsoleMaxLines"); + bool conversionOk = false; + int maxLines = lineSetting->get().toInt(&conversionOk); + if(!conversionOk) + { + maxLines = lineSetting->defValue().toInt(); + qWarning() << "ConsoleMaxLines has nonsensical value, defaulting to" << maxLines; + } + return maxLines; +} + +bool BaseInstance::shouldStopOnConsoleOverflow() const +{ + return settings()->get("ConsoleOverflowStop").toBool(); +} + +void BaseInstance::iconUpdated(QString key) +{ + if(iconKey() == key) + { + emit propertiesChanged(this); + } +} + +void BaseInstance::invalidate() +{ + changeStatus(Status::Gone); + qDebug() << "Instance" << id() << "has been invalidated."; +} + +void BaseInstance::changeStatus(BaseInstance::Status newStatus) +{ + Status status = currentStatus(); + if(status != newStatus) + { + m_status = newStatus; + emit statusChanged(status, newStatus); + } +} + +BaseInstance::Status BaseInstance::currentStatus() const +{ + return m_status; +} + +QString BaseInstance::id() const +{ + return QFileInfo(instanceRoot()).fileName(); +} + +bool BaseInstance::isRunning() const +{ + return m_isRunning; +} + +void BaseInstance::setRunning(bool running) +{ + if(running == m_isRunning) + return; + + m_isRunning = running; + + if(!m_settings->get("RecordGameTime").toBool()) + { + emit runningStatusChanged(running); + return; + } + + if(running) + { + m_timeStarted = QDateTime::currentDateTime(); + } + else + { + QDateTime timeEnded = QDateTime::currentDateTime(); + + qint64 current = settings()->get("totalTimePlayed").toLongLong(); + settings()->set("totalTimePlayed", current + m_timeStarted.secsTo(timeEnded)); + settings()->set("lastTimePlayed", m_timeStarted.secsTo(timeEnded)); + + emit propertiesChanged(this); + } + + emit runningStatusChanged(running); +} + +int64_t BaseInstance::totalTimePlayed() const +{ + qint64 current = settings()->get("totalTimePlayed").toLongLong(); + if(m_isRunning) + { + QDateTime timeNow = QDateTime::currentDateTime(); + return current + m_timeStarted.secsTo(timeNow); + } + return current; +} + +int64_t BaseInstance::lastTimePlayed() const +{ + if(m_isRunning) + { + QDateTime timeNow = QDateTime::currentDateTime(); + return m_timeStarted.secsTo(timeNow); + } + return settings()->get("lastTimePlayed").toLongLong(); +} + +void BaseInstance::resetTimePlayed() +{ + settings()->reset("totalTimePlayed"); + settings()->reset("lastTimePlayed"); +} + +QString BaseInstance::instanceType() const +{ + return m_settings->get("InstanceType").toString(); +} + +QString BaseInstance::instanceRoot() const +{ + return m_rootDir; +} + +SettingsObjectPtr BaseInstance::settings() const +{ + return m_settings; +} + +bool BaseInstance::canLaunch() const +{ + return (!hasVersionBroken() && !isRunning()); +} + +bool BaseInstance::reloadSettings() +{ + return m_settings->reload(); +} + +qint64 BaseInstance::lastLaunch() const +{ + return m_settings->get("lastLaunchTime").value(); +} + +void BaseInstance::setLastLaunch(qint64 val) +{ + //FIXME: if no change, do not set. setting involves saving a file. + m_settings->set("lastLaunchTime", val); + emit propertiesChanged(this); +} + +void BaseInstance::setNotes(QString val) +{ + //FIXME: if no change, do not set. setting involves saving a file. + m_settings->set("notes", val); +} + +QString BaseInstance::notes() const +{ + return m_settings->get("notes").toString(); +} + +void BaseInstance::setIconKey(QString val) +{ + //FIXME: if no change, do not set. setting involves saving a file. + m_settings->set("iconKey", val); + emit propertiesChanged(this); +} + +QString BaseInstance::iconKey() const +{ + return m_settings->get("iconKey").toString(); +} + +void BaseInstance::setName(QString val) +{ + //FIXME: if no change, do not set. setting involves saving a file. + m_settings->set("name", val); + emit propertiesChanged(this); +} + +QString BaseInstance::name() const +{ + return m_settings->get("name").toString(); +} + +QString BaseInstance::windowTitle() const +{ + return BuildConfig.LAUNCHER_NAME + ": " + name().replace(QRegExp("[ \n\r\t]+"), " "); +} + +QString BaseInstance::instanceTitle() const +{ + return name().replace(QRegExp("[ \n\r\t]+"), " "); +} + +// FIXME: why is this here? move it to MinecraftInstance!!! +QStringList BaseInstance::extraArguments() const +{ + return Commandline::splitArgs(settings()->get("JvmArgs").toString()); +} + +shared_qobject_ptr BaseInstance::getLaunchTask() +{ + return m_launchProcess; +} diff --git a/ultimmc/launcher/BaseInstance.h b/ultimmc/launcher/BaseInstance.h new file mode 100644 index 0000000..4e7681c --- /dev/null +++ b/ultimmc/launcher/BaseInstance.h @@ -0,0 +1,284 @@ +/* Copyright 2013-2023 MultiMC Contributors + * Copyright 2022 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +#include +#include "QObjectPtr.h" +#include +#include +#include + +#include "settings/SettingsObject.h" + +#include "settings/INIFile.h" +#include "BaseVersionList.h" +#include "minecraft/auth/MinecraftAccount.h" +#include "MessageLevel.h" +#include "pathmatcher/IPathMatcher.h" + +#include "net/Mode.h" + +#include "minecraft/launch/QuickPlayTarget.h" + +class QDir; +class Task; +class LaunchTask; +class BaseInstance; + +// pointer for lazy people +typedef std::shared_ptr InstancePtr; + +/*! + * \brief Base class for instances. + * This class implements many functions that are common between instances and + * provides a standard interface for all instances. + * + * To create a new instance type, create a new class inheriting from this class + * and implement the pure virtual functions. + */ +class BaseInstance : public QObject, public std::enable_shared_from_this +{ + Q_OBJECT +protected: + /// no-touchy! + BaseInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); + +public: /* types */ + enum class Status + { + Present, + Gone // either nuked or invalidated + }; + +public: + /// virtual destructor to make sure the destruction is COMPLETE + virtual ~BaseInstance() {}; + + virtual void saveNow() = 0; + + /*** + * the instance has been invalidated - it is no longer tracked by the launcher for some reason, + * but it has not necessarily been deleted. + * + * Happens when the instance folder changes to some other location, or the instance is removed by external means. + */ + void invalidate(); + + /// The instance's ID. The ID SHALL be determined by LAUNCHER internally. The ID IS guaranteed to + /// be unique. + virtual QString id() const; + + void setRunning(bool running); + bool isRunning() const; + int64_t totalTimePlayed() const; + int64_t lastTimePlayed() const; + void resetTimePlayed(); + + /// get the type of this instance + QString instanceType() const; + + /// Path to the instance's root directory. + QString instanceRoot() const; + + /// Path to the instance's game root directory. + virtual QString gameRoot() const + { + return instanceRoot(); + } + + /// Path to the instance's mods directory. + virtual QString modsRoot() const = 0; + + QString name() const; + void setName(QString val); + + /// Value used for instance window titles + QString windowTitle() const; + + QString instanceTitle() const; + + QString iconKey() const; + void setIconKey(QString val); + + QString notes() const; + void setNotes(QString val); + + QString getPreLaunchCommand(); + QString getPostExitCommand(); + QString getWrapperCommand(); + + bool isManagedPack(); + QString getManagedPackType(); + QString getManagedPackID(); + QString getManagedPackName(); + QString getManagedPackVersionID(); + QString getManagedPackVersionName(); + void setManagedPack(const QString& type, const QString& id, const QString& name, const QString& versionId, const QString& version); + + /// guess log level from a line of game log + virtual MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) + { + return level; + }; + + virtual QStringList extraArguments() const; + + /// Traits. Normally inside the version, depends on instance implementation. + virtual QSet traits() const = 0; + + /** + * Gets the time that the instance was last launched. + * Stored in milliseconds since epoch. + */ + qint64 lastLaunch() const; + /// Sets the last launched time to 'val' milliseconds since epoch + void setLastLaunch(qint64 val = QDateTime::currentMSecsSinceEpoch()); + + /*! + * \brief Gets this instance's settings object. + * This settings object stores instance-specific settings. + * \return A pointer to this instance's settings object. + */ + virtual SettingsObjectPtr settings() const; + + /// returns a valid update task + virtual Task::Ptr createUpdateTask(Net::Mode mode) = 0; + + /// returns a valid launcher (task container) + virtual shared_qobject_ptr createLaunchTask( + AuthSessionPtr account, QuickPlayTargetPtr quickPlayTarget, quint16 localAuthServerPort) = 0; + + /// returns the current launch task (if any) + shared_qobject_ptr getLaunchTask(); + + /*! + * Create envrironment variables for running the instance + */ + virtual QProcessEnvironment createEnvironment() = 0; + + /*! + * Returns a matcher that can maps relative paths within the instance to whether they are 'log files' + */ + virtual IPathMatcher::Ptr getLogFileMatcher() = 0; + + /*! + * Returns the root folder to use for looking up log files + */ + virtual QString getLogFileRoot() = 0; + + virtual QString getStatusbarDescription() = 0; + + /// FIXME: this really should be elsewhere... + virtual QString instanceConfigFolder() const = 0; + + /// get variables this instance exports + virtual QMap getVariables() const = 0; + + virtual QString typeName() const = 0; + + bool hasVersionBroken() const + { + return m_hasBrokenVersion; + } + void setVersionBroken(bool value) + { + if(m_hasBrokenVersion != value) + { + m_hasBrokenVersion = value; + emit propertiesChanged(this); + } + } + + bool hasUpdateAvailable() const + { + return m_hasUpdate; + } + void setUpdateAvailable(bool value) + { + if(m_hasUpdate != value) + { + m_hasUpdate = value; + emit propertiesChanged(this); + } + } + + bool hasCrashed() const + { + return m_crashed; + } + void setCrashed(bool value) + { + if(m_crashed != value) + { + m_crashed = value; + emit propertiesChanged(this); + } + } + + virtual bool canLaunch() const; + virtual bool canEdit() const = 0; + virtual bool canExport() const = 0; + + bool reloadSettings(); + + /** + * 'print' a verbose description of the instance into a QStringList + */ + virtual QStringList verboseDescription(AuthSessionPtr session, QuickPlayTargetPtr quickPlayTarget) = 0; + + Status currentStatus() const; + + int getConsoleMaxLines() const; + bool shouldStopOnConsoleOverflow() const; + +protected: + void changeStatus(Status newStatus); + +signals: + /*! + * \brief Signal emitted when properties relevant to the instance view change + */ + void propertiesChanged(BaseInstance *inst); + + void launchTaskChanged(shared_qobject_ptr); + + void runningStatusChanged(bool running); + + void statusChanged(Status from, Status to); + +protected slots: + void iconUpdated(QString key); + +protected: /* data */ + QString m_rootDir; + SettingsObjectPtr m_settings; + // InstanceFlags m_flags; + bool m_isRunning = false; + shared_qobject_ptr m_launchProcess; + QDateTime m_timeStarted; + +private: /* data */ + Status m_status = Status::Present; + bool m_crashed = false; + bool m_hasUpdate = false; + bool m_hasBrokenVersion = false; +}; + +Q_DECLARE_METATYPE(shared_qobject_ptr) +//Q_DECLARE_METATYPE(BaseInstance::InstanceFlag) +//Q_DECLARE_OPERATORS_FOR_FLAGS(BaseInstance::InstanceFlags) diff --git a/ultimmc/launcher/BaseVersion.h b/ultimmc/launcher/BaseVersion.h new file mode 100644 index 0000000..b88105f --- /dev/null +++ b/ultimmc/launcher/BaseVersion.h @@ -0,0 +1,59 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +/*! + * An abstract base class for versions. + */ +class BaseVersion +{ +public: + virtual ~BaseVersion() {} + /*! + * A string used to identify this version in config files. + * This should be unique within the version list or shenanigans will occur. + */ + virtual QString descriptor() = 0; + + /*! + * The name of this version as it is displayed to the user. + * For example: "1.5.1" + */ + virtual QString name() = 0; + + /*! + * This should return a string that describes + * the kind of version this is (Stable, Beta, Snapshot, whatever) + */ + virtual QString typeString() const = 0; + + virtual bool operator<(BaseVersion &a) + { + return name() < a.name(); + }; + virtual bool operator>(BaseVersion &a) + { + return name() > a.name(); + }; +}; + +typedef std::shared_ptr BaseVersionPtr; + +Q_DECLARE_METATYPE(BaseVersionPtr) diff --git a/ultimmc/launcher/BaseVersionList.cpp b/ultimmc/launcher/BaseVersionList.cpp new file mode 100644 index 0000000..aa9cb6c --- /dev/null +++ b/ultimmc/launcher/BaseVersionList.cpp @@ -0,0 +1,99 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "BaseVersionList.h" +#include "BaseVersion.h" + +BaseVersionList::BaseVersionList(QObject *parent) : QAbstractListModel(parent) +{ +} + +BaseVersionPtr BaseVersionList::findVersion(const QString &descriptor) +{ + for (int i = 0; i < count(); i++) + { + if (at(i)->descriptor() == descriptor) + return at(i); + } + return BaseVersionPtr(); +} + +BaseVersionPtr BaseVersionList::getRecommended() const +{ + if (count() <= 0) + return BaseVersionPtr(); + else + return at(0); +} + +QVariant BaseVersionList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + BaseVersionPtr version = at(index.row()); + + switch (role) + { + case VersionPointerRole: + return qVariantFromValue(version); + + case VersionRole: + return version->name(); + + case VersionIdRole: + return version->descriptor(); + + case TypeRole: + return version->typeString(); + + default: + return QVariant(); + } +} + +BaseVersionList::RoleList BaseVersionList::providesRoles() const +{ + return {VersionPointerRole, VersionRole, VersionIdRole, TypeRole}; +} + +int BaseVersionList::rowCount(const QModelIndex &parent) const +{ + // Return count + return count(); +} + +int BaseVersionList::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QHash BaseVersionList::roleNames() const +{ + QHash roles = QAbstractListModel::roleNames(); + roles.insert(VersionRole, "version"); + roles.insert(VersionIdRole, "versionId"); + roles.insert(ParentVersionRole, "parentGameVersion"); + roles.insert(RecommendedRole, "recommended"); + roles.insert(LatestRole, "latest"); + roles.insert(TypeRole, "type"); + roles.insert(BranchRole, "branch"); + roles.insert(PathRole, "path"); + roles.insert(ArchitectureRole, "architecture"); + return roles; +} diff --git a/ultimmc/launcher/BaseVersionList.h b/ultimmc/launcher/BaseVersionList.h new file mode 100644 index 0000000..80a91e8 --- /dev/null +++ b/ultimmc/launcher/BaseVersionList.h @@ -0,0 +1,121 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "BaseVersion.h" +#include "tasks/Task.h" +#include "QObjectPtr.h" + +/*! + * \brief Class that each instance type's version list derives from. + * Version lists are the lists that keep track of the available game versions + * for that instance. This list will not be loaded on startup. It will be loaded + * when the list's load function is called. Before using the version list, you + * should check to see if it has been loaded yet and if not, load the list. + * + * Note that this class also inherits from QAbstractListModel. Methods from that + * class determine how this version list shows up in a list view. Said methods + * all have a default implementation, but they can be overridden by plugins to + * change the behavior of the list. + */ +class BaseVersionList : public QAbstractListModel +{ + Q_OBJECT +public: + enum ModelRoles + { + VersionPointerRole = Qt::UserRole, + VersionRole, + VersionIdRole, + ParentVersionRole, + RecommendedRole, + LatestRole, + TypeRole, + BranchRole, + PathRole, + ArchitectureRole, + SortRole + }; + typedef QList RoleList; + + explicit BaseVersionList(QObject *parent = 0); + + /*! + * \brief Gets a task that will reload the version list. + * Simply execute the task to load the list. + * The task returned by this function should reset the model when it's done. + * \return A pointer to a task that reloads the version list. + */ + virtual Task::Ptr getLoadTask() = 0; + + //! Checks whether or not the list is loaded. If this returns false, the list should be + //loaded. + virtual bool isLoaded() = 0; + + //! Gets the version at the given index. + virtual const BaseVersionPtr at(int i) const = 0; + + //! Returns the number of versions in the list. + virtual int count() const = 0; + + //////// List Model Functions //////// + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QHash roleNames() const override; + + //! which roles are provided by this version list? + virtual RoleList providesRoles() const; + + /*! + * \brief Finds a version by its descriptor. + * \param descriptor The descriptor of the version to find. + * \return A const pointer to the version with the given descriptor. NULL if + * one doesn't exist. + */ + virtual BaseVersionPtr findVersion(const QString &descriptor); + + /*! + * \brief Gets the recommended version from this list + * If the list doesn't support recommended versions, this works exactly as getLatestStable + */ + virtual BaseVersionPtr getRecommended() const; + + /*! + * Sorts the version list. + */ + virtual void sortVersions() = 0; + +protected +slots: + /*! + * Updates this list with the given list of versions. + * This is done by copying each version in the given list and inserting it + * into this one. + * We need to do this so that we can set the parents of the versions are set to this + * version list. This can't be done in the load task, because the versions the load + * task creates are on the load task's thread and Qt won't allow their parents + * to be set to something created on another thread. + * To get around that problem, we invoke this method on the GUI thread, which + * then copies the versions and sets their parents correctly. + * \param versions List of versions whose parents should be set. + */ + virtual void updateListData(QList versions) = 0; +}; diff --git a/ultimmc/launcher/CMakeLists.txt b/ultimmc/launcher/CMakeLists.txt new file mode 100644 index 0000000..b895cec --- /dev/null +++ b/ultimmc/launcher/CMakeLists.txt @@ -0,0 +1,1077 @@ +project(application) + +################################ FILES ################################ + +######## Sources and headers ######## + +include (UnitTest) + +set(CORE_SOURCES + # LOGIC - Base classes and infrastructure + AuthServer.h + AuthServer.cpp + BaseInstaller.h + BaseInstaller.cpp + BaseVersionList.h + BaseVersionList.cpp + InstanceList.h + InstanceList.cpp + InstanceTask.h + InstanceTask.cpp + LoggedProcess.h + LoggedProcess.cpp + MessageLevel.cpp + MessageLevel.h + BaseVersion.h + BaseInstance.h + BaseInstance.cpp + NullInstance.h + MMCZip.h + MMCZip.cpp + MMCStrings.h + MMCStrings.cpp + + # Basic instance manipulation tasks (derived from InstanceTask) + InstanceCreationTask.h + InstanceCreationTask.cpp + InstanceCopyTask.h + InstanceCopyTask.cpp + InstanceImportTask.h + InstanceImportTask.cpp + + # Use tracking separate from memory management + Usable.h + + # Prefix tree where node names are strings between separators + SeparatorPrefixTree.h + + # String filters + Filter.h + Filter.cpp + + # JSON parsing helpers + Json.h + Json.cpp + + FileSystem.h + FileSystem.cpp + + Exception.h + + # RW lock protected map + RWStorage.h + + # A variable that has an implicit default value and keeps track of changes + DefaultVariable.h + + # a smart pointer wrapper intended for safer use with Qt signal/slot mechanisms + QObjectPtr.h + + # Compression support + GZip.h + GZip.cpp + + # Command line parameter parsing + Commandline.h + Commandline.cpp + + # Version number string support + Version.h + Version.cpp + + # A Recursive file system watcher + RecursiveFileSystemWatcher.h + RecursiveFileSystemWatcher.cpp + + # Time + MMCTime.h + MMCTime.cpp +) + +add_unit_test(FileSystem + SOURCES FileSystem_test.cpp + LIBS Launcher_logic + DATA testdata + ) + +add_unit_test(GZip + SOURCES GZip_test.cpp + LIBS Launcher_logic + ) + +set(PATHMATCHER_SOURCES + # Path matchers + pathmatcher/FSTreeMatcher.h + pathmatcher/IPathMatcher.h + pathmatcher/MultiMatcher.h + pathmatcher/RegexpMatcher.h +) + +set(NET_SOURCES + # network stuffs + net/ByteArraySink.h + net/ChecksumValidator.h + net/Download.cpp + net/Download.h + net/FileSink.cpp + net/FileSink.h + net/HttpMetaCache.cpp + net/HttpMetaCache.h + net/MetaCacheSink.cpp + net/MetaCacheSink.h + net/NetAction.h + net/NetJob.cpp + net/NetJob.h + net/PasteUpload.cpp + net/PasteUpload.h + net/Sink.h + net/Validator.h +) + +# Game launch logic +set(LAUNCH_SOURCES + launch/steps/CheckJava.cpp + launch/steps/CheckJava.h + launch/steps/LookupServerAddress.cpp + launch/steps/LookupServerAddress.h + launch/steps/PostLaunchCommand.cpp + launch/steps/PostLaunchCommand.h + launch/steps/PreLaunchCommand.cpp + launch/steps/PreLaunchCommand.h + launch/steps/TextPrint.cpp + launch/steps/TextPrint.h + launch/steps/Update.cpp + launch/steps/Update.h + launch/LaunchStep.cpp + launch/LaunchStep.h + launch/LaunchTask.cpp + launch/LaunchTask.h + launch/LogModel.cpp + launch/LogModel.h +) + +# Old update system +set(UPDATE_SOURCES + updater/GoUpdate.h + updater/GoUpdate.cpp + updater/UpdateChecker.h + updater/UpdateChecker.cpp + updater/DownloadTask.h + updater/DownloadTask.cpp +) + +add_unit_test(UpdateChecker + SOURCES updater/UpdateChecker_test.cpp + LIBS Launcher_logic + DATA updater/testdata + ) + +add_unit_test(DownloadTask + SOURCES updater/DownloadTask_test.cpp + LIBS Launcher_logic + DATA updater/testdata + ) + +# Rarely used notifications +set(NOTIFICATIONS_SOURCES + # Notifications - short warning messages + notifications/NotificationChecker.h + notifications/NotificationChecker.cpp +) + +# Backend for the news bar... there's usually no news. +set(NEWS_SOURCES + # News System + news/NewsChecker.h + news/NewsChecker.cpp + news/NewsEntry.h + news/NewsEntry.cpp +) + +# Icon interface +set(ICONS_SOURCES + # Icons System and related code + icons/IconUtils.h + icons/IconUtils.cpp +) + +# Support for Minecraft instances and launch +set(MINECRAFT_SOURCES + # Minecraft support + minecraft/auth/AccountData.cpp + minecraft/auth/AccountData.h + minecraft/auth/AccountList.cpp + minecraft/auth/AccountList.h + minecraft/auth/AccountTask.cpp + minecraft/auth/AccountTask.h + minecraft/auth/AuthProviders.cpp + minecraft/auth/AuthProviders.h + minecraft/auth/AuthRequest.cpp + minecraft/auth/AuthRequest.h + minecraft/auth/AuthSession.cpp + minecraft/auth/AuthSession.h + minecraft/auth/AuthStep.cpp + minecraft/auth/AuthStep.h + minecraft/auth/MinecraftAccount.cpp + minecraft/auth/MinecraftAccount.h + minecraft/auth/Parsers.cpp + minecraft/auth/Parsers.h + minecraft/auth/Yggdrasil.cpp + minecraft/auth/Yggdrasil.h + + minecraft/auth/providers/BaseAuthProvider.h + minecraft/auth/providers/LocalAuthProvider.h + minecraft/auth/providers/ElybyAuthProvider.h + minecraft/auth/providers/MojangAuthProvider.h + minecraft/auth/providers/MicrosoftAuthProvider.h + + minecraft/auth/flows/AuthFlow.cpp + minecraft/auth/flows/AuthFlow.h + minecraft/auth/flows/Mojang.cpp + minecraft/auth/flows/Mojang.h + minecraft/auth/flows/MSA.cpp + minecraft/auth/flows/MSA.h + minecraft/auth/flows/Local.cpp + minecraft/auth/flows/Local.h + minecraft/auth/flows/Elyby.cpp + minecraft/auth/flows/Elyby.h + + minecraft/auth/steps/EntitlementsStep.cpp + minecraft/auth/steps/EntitlementsStep.h + minecraft/auth/steps/ForcedMigrationStep.cpp + minecraft/auth/steps/ForcedMigrationStep.h + minecraft/auth/steps/GetSkinStep.cpp + minecraft/auth/steps/GetSkinStep.h + minecraft/auth/steps/LauncherLoginStep.cpp + minecraft/auth/steps/LauncherLoginStep.h + minecraft/auth/steps/MigrationEligibilityStep.cpp + minecraft/auth/steps/MigrationEligibilityStep.h + minecraft/auth/steps/MinecraftProfileStep.cpp + minecraft/auth/steps/MinecraftProfileStep.h + minecraft/auth/steps/ElybyProfileStep.cpp + minecraft/auth/steps/ElybyProfileStep.h + minecraft/auth/steps/MSAStep.cpp + minecraft/auth/steps/MSAStep.h + minecraft/auth/steps/LocalStep.cpp + minecraft/auth/steps/LocalStep.h + minecraft/auth/steps/XboxAuthorizationStep.cpp + minecraft/auth/steps/XboxAuthorizationStep.h + minecraft/auth/steps/XboxProfileStep.cpp + minecraft/auth/steps/XboxProfileStep.h + minecraft/auth/steps/XboxUserStep.cpp + minecraft/auth/steps/XboxUserStep.h + minecraft/auth/steps/YggdrasilStep.cpp + minecraft/auth/steps/YggdrasilStep.h + + minecraft/gameoptions/GameOptions.h + minecraft/gameoptions/GameOptions.cpp + + minecraft/update/AssetUpdateTask.h + minecraft/update/AssetUpdateTask.cpp + minecraft/update/FMLLibrariesTask.cpp + minecraft/update/FMLLibrariesTask.h + minecraft/update/FoldersTask.cpp + minecraft/update/FoldersTask.h + minecraft/update/LibrariesTask.cpp + minecraft/update/LibrariesTask.h + + minecraft/launch/ClaimAccount.cpp + minecraft/launch/ClaimAccount.h + minecraft/launch/CreateGameFolders.cpp + minecraft/launch/CreateGameFolders.h + minecraft/launch/ModMinecraftJar.cpp + minecraft/launch/ModMinecraftJar.h + minecraft/launch/DirectJavaLaunch.cpp + minecraft/launch/DirectJavaLaunch.h + minecraft/launch/ExtractNatives.cpp + minecraft/launch/ExtractNatives.h + minecraft/launch/LauncherPartLaunch.cpp + minecraft/launch/LauncherPartLaunch.h + minecraft/launch/QuickPlayTarget.cpp + minecraft/launch/QuickPlayTarget.h + minecraft/launch/PrintInstanceInfo.cpp + minecraft/launch/PrintInstanceInfo.h + minecraft/launch/ReconstructAssets.cpp + minecraft/launch/ReconstructAssets.h + minecraft/launch/ScanModFolders.cpp + minecraft/launch/ScanModFolders.h + minecraft/launch/InjectAuthlib.cpp + minecraft/launch/InjectAuthlib.h + minecraft/launch/VerifyJavaInstall.cpp + minecraft/launch/VerifyJavaInstall.h + + minecraft/legacy/LegacyModList.h + minecraft/legacy/LegacyModList.cpp + minecraft/legacy/LegacyInstance.h + minecraft/legacy/LegacyInstance.cpp + minecraft/legacy/LegacyUpgradeTask.h + minecraft/legacy/LegacyUpgradeTask.cpp + + minecraft/GradleSpecifier.h + minecraft/MinecraftInstance.cpp + minecraft/MinecraftInstance.h + minecraft/LaunchProfile.cpp + minecraft/LaunchProfile.h + minecraft/Component.cpp + minecraft/Component.h + minecraft/PackProfile.cpp + minecraft/PackProfile.h + minecraft/ComponentUpdateTask.cpp + minecraft/ComponentUpdateTask.h + minecraft/MinecraftLoadAndCheck.h + minecraft/MinecraftLoadAndCheck.cpp + minecraft/MinecraftUpdate.h + minecraft/MinecraftUpdate.cpp + minecraft/MojangVersionFormat.cpp + minecraft/MojangVersionFormat.h + minecraft/Rule.cpp + minecraft/Rule.h + minecraft/OneSixVersionFormat.cpp + minecraft/OneSixVersionFormat.h + minecraft/OpSys.cpp + minecraft/OpSys.h + minecraft/ParseUtils.cpp + minecraft/ParseUtils.h + minecraft/ProfileUtils.cpp + minecraft/ProfileUtils.h + minecraft/Library.cpp + minecraft/Library.h + minecraft/MojangDownloadInfo.h + minecraft/VersionFile.cpp + minecraft/VersionFile.h + minecraft/VersionFilterData.h + minecraft/VersionFilterData.cpp + minecraft/World.h + minecraft/World.cpp + minecraft/WorldList.h + minecraft/WorldList.cpp + + minecraft/mod/Mod.h + minecraft/mod/Mod.cpp + minecraft/mod/ModDetails.h + minecraft/mod/ModFolderModel.h + minecraft/mod/ModFolderModel.cpp + minecraft/mod/ModFolderLoadTask.h + minecraft/mod/ModFolderLoadTask.cpp + minecraft/mod/LocalModParseTask.h + minecraft/mod/LocalModParseTask.cpp + minecraft/mod/ResourcePackFolderModel.h + minecraft/mod/ResourcePackFolderModel.cpp + minecraft/mod/TexturePackFolderModel.h + minecraft/mod/TexturePackFolderModel.cpp + + # Assets + minecraft/AssetsUtils.h + minecraft/AssetsUtils.cpp + + # Minecraft services + minecraft/services/CapeChange.cpp + minecraft/services/CapeChange.h + minecraft/services/SkinUpload.cpp + minecraft/services/SkinUpload.h + minecraft/services/SkinDelete.cpp + minecraft/services/SkinDelete.h + + mojang/PackageManifest.h + mojang/PackageManifest.cpp + ) + +add_unit_test(GradleSpecifier + SOURCES minecraft/GradleSpecifier_test.cpp + LIBS Launcher_logic + ) + +add_executable(PackageManifest + mojang/PackageManifest_test.cpp +) +target_link_libraries(PackageManifest + Launcher_logic + Qt5::Test +) +target_include_directories(PackageManifest + PRIVATE ../cmake/UnitTest/ +) +add_test( + NAME PackageManifest + COMMAND PackageManifest + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} +) + +add_unit_test(MojangVersionFormat + SOURCES minecraft/MojangVersionFormat_test.cpp + LIBS Launcher_logic + DATA minecraft/testdata + ) + +add_unit_test(Library + SOURCES minecraft/Library_test.cpp + LIBS Launcher_logic + ) + +# FIXME: shares data with FileSystem test +add_unit_test(ModFolderModel + SOURCES minecraft/mod/ModFolderModel_test.cpp + DATA testdata + LIBS Launcher_logic + ) + +add_unit_test(ParseUtils + SOURCES minecraft/ParseUtils_test.cpp + LIBS Launcher_logic + ) + +# the screenshots feature +set(SCREENSHOTS_SOURCES + screenshots/Screenshot.h + screenshots/ImgurUpload.h + screenshots/ImgurUpload.cpp + screenshots/ImgurAlbumCreation.h + screenshots/ImgurAlbumCreation.cpp +) + +set(TASKS_SOURCES + # Tasks + tasks/Task.h + tasks/Task.cpp + tasks/SequentialTask.h + tasks/SequentialTask.cpp +) + +set(SETTINGS_SOURCES + # Settings + settings/INIFile.cpp + settings/INIFile.h + settings/INISettingsObject.cpp + settings/INISettingsObject.h + settings/OverrideSetting.cpp + settings/OverrideSetting.h + settings/PassthroughSetting.cpp + settings/PassthroughSetting.h + settings/Setting.cpp + settings/Setting.h + settings/SettingsObject.cpp + settings/SettingsObject.h +) + +add_unit_test(INIFile + SOURCES settings/INIFile_test.cpp + LIBS Launcher_logic + ) + +set(JAVA_SOURCES + java/JavaChecker.h + java/JavaChecker.cpp + java/JavaCheckerJob.h + java/JavaCheckerJob.cpp + java/JavaInstall.h + java/JavaInstall.cpp + java/JavaInstallList.h + java/JavaInstallList.cpp + java/JavaUtils.h + java/JavaUtils.cpp + java/JavaVersion.h + java/JavaVersion.cpp +) + +add_unit_test(JavaVersion + SOURCES java/JavaVersion_test.cpp + LIBS Launcher_logic + ) + +set(TRANSLATIONS_SOURCES + translations/TranslationsModel.h + translations/TranslationsModel.cpp + translations/POTranslator.h + translations/POTranslator.cpp +) + +set(TOOLS_SOURCES + # Tools + tools/BaseExternalTool.cpp + tools/BaseExternalTool.h + tools/BaseProfiler.cpp + tools/BaseProfiler.h + tools/JProfiler.cpp + tools/JProfiler.h + tools/JVisualVM.cpp + tools/JVisualVM.h + tools/MCEditTool.cpp + tools/MCEditTool.h +) + +set(META_SOURCES + # Metadata sources + meta/JsonFormat.cpp + meta/JsonFormat.h + meta/BaseEntity.cpp + meta/BaseEntity.h + meta/VersionList.cpp + meta/VersionList.h + meta/Version.cpp + meta/Version.h + meta/Index.cpp + meta/Index.h +) + +set(FTB_SOURCES + modplatform/legacy_ftb/PackFetchTask.h + modplatform/legacy_ftb/PackFetchTask.cpp + modplatform/legacy_ftb/PackInstallTask.h + modplatform/legacy_ftb/PackInstallTask.cpp + modplatform/legacy_ftb/PrivatePackManager.h + modplatform/legacy_ftb/PrivatePackManager.cpp + + modplatform/legacy_ftb/PackHelpers.h +) + +set(TECHNIC_SOURCES + modplatform/technic/SingleZipPackInstallTask.h + modplatform/technic/SingleZipPackInstallTask.cpp + modplatform/technic/SolderPackInstallTask.h + modplatform/technic/SolderPackInstallTask.cpp + modplatform/technic/SolderPackManifest.h + modplatform/technic/SolderPackManifest.cpp + modplatform/technic/TechnicPackProcessor.h + modplatform/technic/TechnicPackProcessor.cpp +) + +set(ATLAUNCHER_SOURCES + modplatform/atlauncher/ATLPackIndex.cpp + modplatform/atlauncher/ATLPackIndex.h + modplatform/atlauncher/ATLPackInstallTask.cpp + modplatform/atlauncher/ATLPackInstallTask.h + modplatform/atlauncher/ATLPackManifest.cpp + modplatform/atlauncher/ATLPackManifest.h +) + +set(MODRINTH_SOURCES + modplatform/modrinth/ModrinthPackManifest.cpp + modplatform/modrinth/ModrinthPackManifest.h + modplatform/modrinth/ModrinthInstanceExportTask.h + modplatform/modrinth/ModrinthInstanceExportTask.cpp + modplatform/modrinth/ModrinthHashLookupRequest.h + modplatform/modrinth/ModrinthHashLookupRequest.cpp +) + +add_unit_test(Index + SOURCES meta/Index_test.cpp + LIBS Launcher_logic + ) + +################################ COMPILE ################################ + +# we need zlib +find_package(ZLIB REQUIRED) + +set(LOGIC_SOURCES + ${CORE_SOURCES} + ${PATHMATCHER_SOURCES} + ${NET_SOURCES} + ${LAUNCH_SOURCES} + ${UPDATE_SOURCES} + ${NOTIFICATIONS_SOURCES} + ${NEWS_SOURCES} + ${MINECRAFT_SOURCES} + ${SCREENSHOTS_SOURCES} + ${TASKS_SOURCES} + ${SETTINGS_SOURCES} + ${JAVA_SOURCES} + ${TRANSLATIONS_SOURCES} + ${TOOLS_SOURCES} + ${META_SOURCES} + ${ICONS_SOURCES} + ${FTB_SOURCES} + ${FTBA_SOURCES} + ${TECHNIC_SOURCES} + ${ATLAUNCHER_SOURCES} + ${MODRINTH_SOURCES} +) + +SET(LAUNCHER_SOURCES + # Application base + Application.h + Application.cpp + UpdateController.cpp + UpdateController.h + ApplicationMessage.h + ApplicationMessage.cpp + + # GUI - general utilities + DesktopServices.h + DesktopServices.cpp + VersionProxyModel.h + VersionProxyModel.cpp + HoeDown.h + + # Super secret! + KonamiCode.h + KonamiCode.cpp + + # Bundled resources + resources/backgrounds/backgrounds.qrc + resources/multimc/multimc.qrc + resources/pe_dark/pe_dark.qrc + resources/pe_light/pe_light.qrc + resources/pe_colored/pe_colored.qrc + resources/pe_blue/pe_blue.qrc + resources/OSX/OSX.qrc + resources/iOS/iOS.qrc + resources/flat/flat.qrc + resources/documents/documents.qrc + ../${Launcher_Branding_LogoQRC} + + # Icons + icons/MMCIcon.h + icons/MMCIcon.cpp + icons/IconList.h + icons/IconList.cpp + + # GUI - windows + ui/GuiUtil.h + ui/GuiUtil.cpp + ui/ColorCache.h + ui/ColorCache.cpp + ui/MainWindow.h + ui/MainWindow.cpp + ui/InstanceWindow.h + ui/InstanceWindow.cpp + + # FIXME: maybe find a better home for this. + SkinUtils.cpp + SkinUtils.h + + # GUI - setup wizard + ui/setupwizard/SetupWizard.h + ui/setupwizard/SetupWizard.cpp + ui/setupwizard/AnalyticsWizardPage.cpp + ui/setupwizard/AnalyticsWizardPage.h + ui/setupwizard/BaseWizardPage.h + ui/setupwizard/JavaWizardPage.cpp + ui/setupwizard/JavaWizardPage.h + ui/setupwizard/LanguageWizardPage.cpp + ui/setupwizard/LanguageWizardPage.h + + # GUI - themes + ui/themes/FusionTheme.cpp + ui/themes/FusionTheme.h + ui/themes/BrightTheme.cpp + ui/themes/BrightTheme.h + ui/themes/CustomTheme.cpp + ui/themes/CustomTheme.h + ui/themes/DarkTheme.cpp + ui/themes/DarkTheme.h + ui/themes/ITheme.cpp + ui/themes/ITheme.h + ui/themes/SystemTheme.cpp + ui/themes/SystemTheme.h + + # Processes + LaunchController.h + LaunchController.cpp + + # page provider for instances + InstancePageProvider.h + + # Common java checking UI + JavaCommon.h + JavaCommon.cpp + + # GUI - paged dialog base + ui/pages/BasePage.h + ui/pages/BasePageContainer.h + ui/pages/BasePageProvider.h + + # GUI - instance pages + ui/pages/instance/GameOptionsPage.cpp + ui/pages/instance/GameOptionsPage.h + ui/pages/instance/VersionPage.cpp + ui/pages/instance/VersionPage.h + ui/pages/instance/TexturePackPage.h + ui/pages/instance/ResourcePackPage.h + ui/pages/instance/ShaderPackPage.h + ui/pages/instance/ModFolderPage.cpp + ui/pages/instance/ModFolderPage.h + ui/pages/instance/NotesPage.cpp + ui/pages/instance/NotesPage.h + ui/pages/instance/LogPage.cpp + ui/pages/instance/LogPage.h + ui/pages/instance/InstanceSettingsPage.cpp + ui/pages/instance/InstanceSettingsPage.h + ui/pages/instance/ScreenshotsPage.cpp + ui/pages/instance/ScreenshotsPage.h + ui/pages/instance/OtherLogsPage.cpp + ui/pages/instance/OtherLogsPage.h + ui/pages/instance/ServersPage.cpp + ui/pages/instance/ServersPage.h + ui/pages/instance/LegacyUpgradePage.cpp + ui/pages/instance/LegacyUpgradePage.h + ui/pages/instance/WorldListPage.cpp + ui/pages/instance/WorldListPage.h + + # GUI - global settings pages + ui/pages/global/AccountListPage.cpp + ui/pages/global/AccountListPage.h + ui/pages/global/CustomCommandsPage.cpp + ui/pages/global/CustomCommandsPage.h + ui/pages/global/ExternalToolsPage.cpp + ui/pages/global/ExternalToolsPage.h + ui/pages/global/JavaPage.cpp + ui/pages/global/JavaPage.h + ui/pages/global/LanguagePage.cpp + ui/pages/global/LanguagePage.h + ui/pages/global/MinecraftPage.cpp + ui/pages/global/MinecraftPage.h + ui/pages/global/LauncherPage.cpp + ui/pages/global/LauncherPage.h + ui/pages/global/ProxyPage.cpp + ui/pages/global/ProxyPage.h + ui/pages/global/PasteEEPage.cpp + ui/pages/global/PasteEEPage.h + + # GUI - platform pages + ui/pages/modplatform/VanillaPage.cpp + ui/pages/modplatform/VanillaPage.h + + ui/pages/modplatform/atlauncher/AtlFilterModel.cpp + ui/pages/modplatform/atlauncher/AtlFilterModel.h + ui/pages/modplatform/atlauncher/AtlListModel.cpp + ui/pages/modplatform/atlauncher/AtlListModel.h + ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp + ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h + ui/pages/modplatform/atlauncher/AtlPage.cpp + ui/pages/modplatform/atlauncher/AtlPage.h + + ui/pages/modplatform/legacy_ftb/Page.cpp + ui/pages/modplatform/legacy_ftb/Page.h + ui/pages/modplatform/legacy_ftb/ListModel.h + ui/pages/modplatform/legacy_ftb/ListModel.cpp + + ui/pages/modplatform/import_ftb/PackInstallTask.cpp + ui/pages/modplatform/import_ftb/PackInstallTask.h + ui/pages/modplatform/import_ftb/Model.cpp + ui/pages/modplatform/import_ftb/Model.h + ui/pages/modplatform/import_ftb/FTBAPage.cpp + ui/pages/modplatform/import_ftb/FTBAPage.h + + ui/pages/modplatform/modrinth/ModrinthData.h + ui/pages/modplatform/modrinth/ModrinthModel.cpp + ui/pages/modplatform/modrinth/ModrinthModel.h + ui/pages/modplatform/modrinth/ModrinthDocument.cpp + ui/pages/modplatform/modrinth/ModrinthDocument.h + ui/pages/modplatform/modrinth/ModrinthPage.cpp + ui/pages/modplatform/modrinth/ModrinthPage.h + + ui/pages/modplatform/technic/TechnicModel.cpp + ui/pages/modplatform/technic/TechnicModel.h + ui/pages/modplatform/technic/TechnicPage.cpp + ui/pages/modplatform/technic/TechnicPage.h + + ui/pages/modplatform/ImportPage.cpp + ui/pages/modplatform/ImportPage.h + + # GUI - dialogs + ui/dialogs/AboutDialog.cpp + ui/dialogs/AboutDialog.h + ui/dialogs/ProfileSelectDialog.cpp + ui/dialogs/ProfileSelectDialog.h + ui/dialogs/ProfileSetupDialog.cpp + ui/dialogs/ProfileSetupDialog.h + ui/dialogs/CopyInstanceDialog.cpp + ui/dialogs/CopyInstanceDialog.h + ui/dialogs/CustomMessageBox.cpp + ui/dialogs/CustomMessageBox.h + ui/dialogs/EditAccountDialog.cpp + ui/dialogs/EditAccountDialog.h + ui/dialogs/ExportInstanceDialog.cpp + ui/dialogs/ExportInstanceDialog.h + ui/dialogs/IconPickerDialog.cpp + ui/dialogs/IconPickerDialog.h + ui/dialogs/LoginDialog.cpp + ui/dialogs/LoginDialog.h + ui/dialogs/MSALoginDialog.cpp + ui/dialogs/MSALoginDialog.h + ui/dialogs/LocalLoginDialog.cpp + ui/dialogs/LocalLoginDialog.h + ui/dialogs/NewComponentDialog.cpp + ui/dialogs/NewComponentDialog.h + ui/dialogs/NewInstanceDialog.cpp + ui/dialogs/NewInstanceDialog.h + ui/dialogs/NotificationDialog.cpp + ui/dialogs/NotificationDialog.h + ui/pagedialog/PageDialog.cpp + ui/pagedialog/PageDialog.h + ui/dialogs/ProgressDialog.cpp + ui/dialogs/ProgressDialog.h + ui/dialogs/UpdateDialog.cpp + ui/dialogs/UpdateDialog.h + ui/dialogs/VersionSelectDialog.cpp + ui/dialogs/VersionSelectDialog.h + ui/dialogs/SkinUploadDialog.cpp + ui/dialogs/SkinUploadDialog.h + ui/dialogs/CreateShortcutDialog.cpp + ui/dialogs/CreateShortcutDialog.h + ui/dialogs/ModrinthExportDialog.cpp + ui/dialogs/ModrinthExportDialog.h + + # GUI - widgets + ui/widgets/Common.cpp + ui/widgets/Common.h + ui/widgets/CustomCommands.cpp + ui/widgets/CustomCommands.h + ui/widgets/DropLabel.cpp + ui/widgets/DropLabel.h + ui/widgets/FocusLineEdit.cpp + ui/widgets/FocusLineEdit.h + ui/widgets/IconLabel.cpp + ui/widgets/IconLabel.h + ui/widgets/JavaSettingsWidget.cpp + ui/widgets/JavaSettingsWidget.h + ui/widgets/LabeledToolButton.cpp + ui/widgets/LabeledToolButton.h + ui/widgets/LanguageSelectionWidget.cpp + ui/widgets/LanguageSelectionWidget.h + ui/widgets/LineSeparator.cpp + ui/widgets/LineSeparator.h + ui/widgets/LogView.cpp + ui/widgets/LogView.h + ui/widgets/MCModInfoFrame.cpp + ui/widgets/MCModInfoFrame.h + ui/widgets/ModListView.cpp + ui/widgets/ModListView.h + ui/widgets/PageContainer.cpp + ui/widgets/PageContainer.h + ui/widgets/PageContainer_p.h + ui/widgets/VersionListView.cpp + ui/widgets/VersionListView.h + ui/widgets/VersionSelectWidget.cpp + ui/widgets/VersionSelectWidget.h + ui/widgets/ProgressWidget.h + ui/widgets/ProgressWidget.cpp + ui/widgets/WideBar.h + ui/widgets/WideBar.cpp + + # GUI - instance group view + ui/instanceview/InstanceProxyModel.cpp + ui/instanceview/InstanceProxyModel.h + ui/instanceview/AccessibleInstanceView.cpp + ui/instanceview/AccessibleInstanceView.h + ui/instanceview/AccessibleInstanceView_p.h + ui/instanceview/InstanceView.cpp + ui/instanceview/InstanceView.h + ui/instanceview/InstanceDelegate.cpp + ui/instanceview/InstanceDelegate.h + ui/instanceview/VisualGroup.cpp + ui/instanceview/VisualGroup.h +) + +qt5_wrap_ui(LAUNCHER_UI + ui/pages/global/AccountListPage.ui + ui/pages/global/JavaPage.ui + ui/pages/global/LauncherPage.ui + ui/pages/global/PasteEEPage.ui + ui/pages/global/ProxyPage.ui + ui/pages/global/MinecraftPage.ui + ui/pages/global/ExternalToolsPage.ui + ui/pages/instance/ModFolderPage.ui + ui/pages/instance/NotesPage.ui + ui/pages/instance/LogPage.ui + ui/pages/instance/ServersPage.ui + ui/pages/instance/GameOptionsPage.ui + ui/pages/instance/OtherLogsPage.ui + ui/pages/instance/InstanceSettingsPage.ui + ui/pages/instance/VersionPage.ui + ui/pages/instance/WorldListPage.ui + ui/pages/instance/LegacyUpgradePage.ui + ui/pages/instance/ScreenshotsPage.ui + ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui + ui/pages/modplatform/atlauncher/AtlPage.ui + ui/pages/modplatform/VanillaPage.ui + ui/pages/modplatform/legacy_ftb/Page.ui + ui/pages/modplatform/import_ftb/FTBAPage.ui + ui/pages/modplatform/ImportPage.ui + ui/pages/modplatform/modrinth/ModrinthPage.ui + ui/pages/modplatform/technic/TechnicPage.ui + ui/widgets/InstanceCardWidget.ui + ui/widgets/CustomCommands.ui + ui/widgets/MCModInfoFrame.ui + ui/dialogs/CopyInstanceDialog.ui + ui/dialogs/ProfileSetupDialog.ui + ui/dialogs/ProgressDialog.ui + ui/dialogs/NewInstanceDialog.ui + ui/dialogs/NotificationDialog.ui + ui/dialogs/UpdateDialog.ui + ui/dialogs/NewComponentDialog.ui + ui/dialogs/ProfileSelectDialog.ui + ui/dialogs/SkinUploadDialog.ui + ui/dialogs/ExportInstanceDialog.ui + ui/dialogs/IconPickerDialog.ui + ui/dialogs/MSALoginDialog.ui + ui/dialogs/AboutDialog.ui + ui/dialogs/LocalLoginDialog.ui + ui/dialogs/LoginDialog.ui + ui/dialogs/EditAccountDialog.ui + ui/dialogs/CreateShortcutDialog.ui + ui/dialogs/ModrinthExportDialog.ui +) + +qt5_add_resources(LAUNCHER_RESOURCES + resources/backgrounds/backgrounds.qrc + resources/multimc/multimc.qrc + resources/pe_dark/pe_dark.qrc + resources/pe_light/pe_light.qrc + resources/pe_colored/pe_colored.qrc + resources/pe_blue/pe_blue.qrc + resources/OSX/OSX.qrc + resources/iOS/iOS.qrc + resources/flat/flat.qrc + resources/documents/documents.qrc + ../${Launcher_Branding_LogoQRC} +) + +######## Windows resource files ######## +if(WIN32) + set(LAUNCHER_RCS ../${Launcher_Branding_WindowsRC}) +endif() + +# Add executable +add_library(Launcher_logic STATIC ${LOGIC_SOURCES} ${LAUNCHER_SOURCES} ${LAUNCHER_UI} ${LAUNCHER_RESOURCES}) +target_link_libraries(Launcher_logic + systeminfo + Launcher_quazip + Launcher_classparser + ${NBT_NAME} + ${ZLIB_LIBRARIES} + optional-bare + tomlc99 + BuildConfig + Katabasis +) +target_link_libraries(Launcher_logic + Qt5::Core + Qt5::Xml + Qt5::Network + Qt5::Concurrent + Qt5::Gui +) +target_link_libraries(Launcher_logic + Launcher_iconfix + ${QUAZIP_LIBRARIES} + hoedown + Launcher_rainbow + LocalPeer + ganalytics +) + +target_link_libraries(Launcher_logic secrets) + +add_executable(${Launcher_Name} MACOSX_BUNDLE WIN32 main.cpp ${LAUNCHER_RCS}) +target_link_libraries(${Launcher_Name} Launcher_logic) + +if(DEFINED Launcher_APP_BINARY_NAME) + set_target_properties(${Launcher_Name} PROPERTIES OUTPUT_NAME "${Launcher_APP_BINARY_NAME}") +endif() +if(DEFINED Launcher_BINARY_RPATH) + SET_TARGET_PROPERTIES(${Launcher_Name} PROPERTIES INSTALL_RPATH "${Launcher_BINARY_RPATH}") +endif() + +if(DEFINED Launcher_APP_BINARY_DEFS) + target_compile_definitions(${Launcher_Name} PRIVATE ${Launcher_APP_BINARY_DEFS}) + target_compile_definitions(Launcher_logic PRIVATE ${Launcher_APP_BINARY_DEFS}) +endif() + +install(TARGETS ${Launcher_Name} + BUNDLE DESTINATION ${BUNDLE_DEST_DIR} COMPONENT Runtime + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} COMPONENT Runtime + RUNTIME DESTINATION ${BINARY_DEST_DIR} COMPONENT Runtime +) + +#### The bundle mess! #### +# Bundle utilities are used to complete the portable packages - they add all the libraries that would otherwise be missing on the target system. +# NOTE: it seems that this absolutely has to be here, and nowhere else. +if(INSTALL_BUNDLE STREQUAL "full") + # Add qt.conf - this makes Qt stop looking for things outside the bundle + install( + CODE "file(WRITE \"\${CMAKE_INSTALL_PREFIX}/${RESOURCES_DEST_DIR}/qt.conf\" \" \")" + COMPONENT Runtime + ) + # Bundle plugins + if(CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "RelWithDebInfo") + # Image formats + install( + DIRECTORY "${QT_PLUGINS_DIR}/imageformats" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "tga|tiff|mng|webp" EXCLUDE + ) + # Icon engines + install( + DIRECTORY "${QT_PLUGINS_DIR}/iconengines" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "fontawesome" EXCLUDE + ) + # Platform plugins + install( + DIRECTORY "${QT_PLUGINS_DIR}/platforms" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "minimal|linuxfb|offscreen" EXCLUDE + ) + # Style plugins + if(EXISTS "${QT_PLUGINS_DIR}/styles") + install( + DIRECTORY "${QT_PLUGINS_DIR}/styles" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + ) + endif() + else() + # Image formats + install( + DIRECTORY "${QT_PLUGINS_DIR}/imageformats" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "tga|tiff|mng|webp" EXCLUDE + REGEX "d\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + # Icon engines + install( + DIRECTORY "${QT_PLUGINS_DIR}/iconengines" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "fontawesome" EXCLUDE + REGEX "d\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + # Platform plugins + install( + DIRECTORY "${QT_PLUGINS_DIR}/platforms" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "minimal|linuxfb|offscreen" EXCLUDE + REGEX "d\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + # Style plugins + if(EXISTS "${QT_PLUGINS_DIR}/styles") + install( + DIRECTORY "${QT_PLUGINS_DIR}/styles" + DESTINATION ${PLUGIN_DEST_DIR} + COMPONENT Runtime + REGEX "d\\." EXCLUDE + REGEX "_debug\\." EXCLUDE + REGEX "\\.dSYM" EXCLUDE + ) + endif() + endif() + configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/install_prereqs.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" + @ONLY + ) + install(SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/install_prereqs.cmake" COMPONENT Runtime) +endif() diff --git a/ultimmc/launcher/Commandline.cpp b/ultimmc/launcher/Commandline.cpp new file mode 100644 index 0000000..2c0fde6 --- /dev/null +++ b/ultimmc/launcher/Commandline.cpp @@ -0,0 +1,483 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Authors: Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Commandline.h" + +/** + * @file libutil/src/cmdutils.cpp + */ + +namespace Commandline +{ + +// commandline splitter +QStringList splitArgs(QString args) +{ + QStringList argv; + QString current; + bool escape = false; + QChar inquotes; + for (int i = 0; i < args.length(); i++) + { + QChar cchar = args.at(i); + + // \ escaped + if (escape) + { + current += cchar; + escape = false; + // in "quotes" + } + else if (!inquotes.isNull()) + { + if (cchar == '\\') + escape = true; + else if (cchar == inquotes) + inquotes = 0; + else + current += cchar; + // otherwise + } + else + { + if (cchar == ' ') + { + if (!current.isEmpty()) + { + argv << current; + current.clear(); + } + } + else if (cchar == '"' || cchar == '\'') + inquotes = cchar; + else + current += cchar; + } + } + if (!current.isEmpty()) + argv << current; + return argv; +} + +Parser::Parser(FlagStyle::Enum flagStyle, ArgumentStyle::Enum argStyle) +{ + m_flagStyle = flagStyle; + m_argStyle = argStyle; +} + +// styles setter/getter +void Parser::setArgumentStyle(ArgumentStyle::Enum style) +{ + m_argStyle = style; +} +ArgumentStyle::Enum Parser::argumentStyle() +{ + return m_argStyle; +} + +void Parser::setFlagStyle(FlagStyle::Enum style) +{ + m_flagStyle = style; +} +FlagStyle::Enum Parser::flagStyle() +{ + return m_flagStyle; +} + +// setup methods +void Parser::addSwitch(QString name, bool def) +{ + if (m_params.contains(name)) + throw "Name not unique"; + + OptionDef *param = new OptionDef; + param->type = otSwitch; + param->name = name; + param->metavar = QString("<%1>").arg(name); + param->def = def; + + m_options[name] = param; + m_params[name] = (CommonDef *)param; + m_optionList.append(param); +} + +void Parser::addOption(QString name, QVariant def) +{ + if (m_params.contains(name)) + throw "Name not unique"; + + OptionDef *param = new OptionDef; + param->type = otOption; + param->name = name; + param->metavar = QString("<%1>").arg(name); + param->def = def; + + m_options[name] = param; + m_params[name] = (CommonDef *)param; + m_optionList.append(param); +} + +void Parser::addArgument(QString name, bool required, QVariant def) +{ + if (m_params.contains(name)) + throw "Name not unique"; + + PositionalDef *param = new PositionalDef; + param->name = name; + param->def = def; + param->required = required; + param->metavar = name; + + m_positionals.append(param); + m_params[name] = (CommonDef *)param; +} + +void Parser::addDocumentation(QString name, QString doc, QString metavar) +{ + if (!m_params.contains(name)) + throw "Name does not exist"; + + CommonDef *param = m_params[name]; + param->doc = doc; + if (!metavar.isNull()) + param->metavar = metavar; +} + +void Parser::addShortOpt(QString name, QChar flag) +{ + if (!m_params.contains(name)) + throw "Name does not exist"; + if (!m_options.contains(name)) + throw "Name is not an Option or Swtich"; + + OptionDef *param = m_options[name]; + m_flags[flag] = param; + param->flag = flag; +} + +// help methods +QString Parser::compileHelp(QString progName, int helpIndent, bool useFlags) +{ + QStringList help; + help << compileUsage(progName, useFlags) << "\r\n"; + + // positionals + if (!m_positionals.isEmpty()) + { + help << "\r\n"; + help << "Positional arguments:\r\n"; + QListIterator it2(m_positionals); + while (it2.hasNext()) + { + PositionalDef *param = it2.next(); + help << " " << param->metavar; + help << " " << QString(helpIndent - param->metavar.length() - 1, ' '); + help << param->doc << "\r\n"; + } + } + + // Options + if (!m_optionList.isEmpty()) + { + help << "\r\n"; + QString optPrefix, flagPrefix; + getPrefix(optPrefix, flagPrefix); + + help << "Options & Switches:\r\n"; + QListIterator it(m_optionList); + while (it.hasNext()) + { + OptionDef *option = it.next(); + help << " "; + int nameLength = optPrefix.length() + option->name.length(); + if (!option->flag.isNull()) + { + nameLength += 3 + flagPrefix.length(); + help << flagPrefix << option->flag << ", "; + } + help << optPrefix << option->name; + if (option->type == otOption) + { + QString arg = QString("%1%2").arg( + ((m_argStyle == ArgumentStyle::Equals) ? "=" : " "), option->metavar); + nameLength += arg.length(); + help << arg; + } + help << " " << QString(helpIndent - nameLength - 1, ' '); + help << option->doc << "\r\n"; + } + } + + return help.join(""); +} + +QString Parser::compileUsage(QString progName, bool useFlags) +{ + QStringList usage; + usage << "Usage: " << progName; + + QString optPrefix, flagPrefix; + getPrefix(optPrefix, flagPrefix); + + // options + QListIterator it(m_optionList); + while (it.hasNext()) + { + OptionDef *option = it.next(); + usage << " ["; + if (!option->flag.isNull() && useFlags) + usage << flagPrefix << option->flag; + else + usage << optPrefix << option->name; + if (option->type == otOption) + usage << ((m_argStyle == ArgumentStyle::Equals) ? "=" : " ") << option->metavar; + usage << "]"; + } + + // arguments + QListIterator it2(m_positionals); + while (it2.hasNext()) + { + PositionalDef *param = it2.next(); + usage << " " << (param->required ? "<" : "["); + usage << param->metavar; + usage << (param->required ? ">" : "]"); + } + + return usage.join(""); +} + +// parsing +QHash Parser::parse(QStringList argv) +{ + QHash map; + + QStringListIterator it(argv); + QString programName = it.next(); + + QString optionPrefix; + QString flagPrefix; + QListIterator positionals(m_positionals); + QStringList expecting; + + getPrefix(optionPrefix, flagPrefix); + + while (it.hasNext()) + { + QString arg = it.next(); + + if (!expecting.isEmpty()) + // we were expecting an argument + { + QString name = expecting.first(); +/* + if (map.contains(name)) + throw ParsingError( + QString("Option %2%1 was given multiple times").arg(name, optionPrefix)); +*/ + map[name] = QVariant(arg); + + expecting.removeFirst(); + continue; + } + + if (arg.startsWith(optionPrefix)) + // we have an option + { + // qDebug("Found option %s", qPrintable(arg)); + + QString name = arg.mid(optionPrefix.length()); + QString equals; + + if ((m_argStyle == ArgumentStyle::Equals || + m_argStyle == ArgumentStyle::SpaceAndEquals) && + name.contains("=")) + { + int i = name.indexOf("="); + equals = name.mid(i + 1); + name = name.left(i); + } + + if (m_options.contains(name)) + { + /* + if (map.contains(name)) + throw ParsingError(QString("Option %2%1 was given multiple times") + .arg(name, optionPrefix)); +*/ + OptionDef *option = m_options[name]; + if (option->type == otSwitch) + map[name] = true; + else // if (option->type == otOption) + { + if (m_argStyle == ArgumentStyle::Space) + expecting.append(name); + else if (!equals.isNull()) + map[name] = equals; + else if (m_argStyle == ArgumentStyle::SpaceAndEquals) + expecting.append(name); + else + throw ParsingError(QString("Option %2%1 reqires an argument.") + .arg(name, optionPrefix)); + } + + continue; + } + + throw ParsingError(QString("Unknown Option %2%1").arg(name, optionPrefix)); + } + + if (arg.startsWith(flagPrefix)) + // we have (a) flag(s) + { + // qDebug("Found flags %s", qPrintable(arg)); + + QString flags = arg.mid(flagPrefix.length()); + QString equals; + + if ((m_argStyle == ArgumentStyle::Equals || + m_argStyle == ArgumentStyle::SpaceAndEquals) && + flags.contains("=")) + { + int i = flags.indexOf("="); + equals = flags.mid(i + 1); + flags = flags.left(i); + } + + for (int i = 0; i < flags.length(); i++) + { + QChar flag = flags.at(i); + + if (!m_flags.contains(flag)) + throw ParsingError(QString("Unknown flag %2%1").arg(flag, flagPrefix)); + + OptionDef *option = m_flags[flag]; +/* + if (map.contains(option->name)) + throw ParsingError(QString("Option %2%1 was given multiple times") + .arg(option->name, optionPrefix)); +*/ + if (option->type == otSwitch) + map[option->name] = true; + else // if (option->type == otOption) + { + if (m_argStyle == ArgumentStyle::Space) + expecting.append(option->name); + else if (!equals.isNull()) + if (i == flags.length() - 1) + map[option->name] = equals; + else + throw ParsingError(QString("Flag %4%2 of Argument-requiring Option " + "%1 not last flag in %4%3") + .arg(option->name, flag, flags, flagPrefix)); + else if (m_argStyle == ArgumentStyle::SpaceAndEquals) + expecting.append(option->name); + else + throw ParsingError(QString("Option %1 reqires an argument. (flag %3%2)") + .arg(option->name, flag, flagPrefix)); + } + } + + continue; + } + + // must be a positional argument + if (!positionals.hasNext()) + throw ParsingError(QString("Don't know what to do with '%1'").arg(arg)); + + PositionalDef *param = positionals.next(); + + map[param->name] = arg; + } + + // check if we're missing something + if (!expecting.isEmpty()) + throw ParsingError(QString("Was still expecting arguments for %2%1").arg( + expecting.join(QString(", ") + optionPrefix), optionPrefix)); + + while (positionals.hasNext()) + { + PositionalDef *param = positionals.next(); + if (param->required) + throw ParsingError( + QString("Missing required positional argument '%1'").arg(param->name)); + else + map[param->name] = param->def; + } + + // fill out gaps + QListIterator iter(m_optionList); + while (iter.hasNext()) + { + OptionDef *option = iter.next(); + if (!map.contains(option->name)) + map[option->name] = option->def; + } + + return map; +} + +// clear defs +void Parser::clear() +{ + m_flags.clear(); + m_params.clear(); + m_options.clear(); + + QMutableListIterator it(m_optionList); + while (it.hasNext()) + { + OptionDef *option = it.next(); + it.remove(); + delete option; + } + + QMutableListIterator it2(m_positionals); + while (it2.hasNext()) + { + PositionalDef *arg = it2.next(); + it2.remove(); + delete arg; + } +} + +// Destructor +Parser::~Parser() +{ + clear(); +} + +// getPrefix +void Parser::getPrefix(QString &opt, QString &flag) +{ + if (m_flagStyle == FlagStyle::Windows) + opt = flag = "/"; + else if (m_flagStyle == FlagStyle::Unix) + opt = flag = "-"; + // else if (m_flagStyle == FlagStyle::GNU) + else + { + opt = "--"; + flag = "-"; + } +} + +// ParsingError +ParsingError::ParsingError(const QString &what) : std::runtime_error(what.toStdString()) +{ +} +} \ No newline at end of file diff --git a/ultimmc/launcher/Commandline.h b/ultimmc/launcher/Commandline.h new file mode 100644 index 0000000..a4e7aa6 --- /dev/null +++ b/ultimmc/launcher/Commandline.h @@ -0,0 +1,250 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Authors: Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include +#include +#include +#include + +/** + * @file libutil/include/cmdutils.h + * @brief commandline parsing and processing utilities + */ + +namespace Commandline +{ + +/** + * @brief split a string into argv items like a shell would do + * @param args the argument string + * @return a QStringList containing all arguments + */ +QStringList splitArgs(QString args); + +/** + * @brief The FlagStyle enum + * Specifies how flags are decorated + */ + +namespace FlagStyle +{ +enum Enum +{ + GNU, /**< --option and -o (GNU Style) */ + Unix, /**< -option and -o (Unix Style) */ + Windows, /**< /option and /o (Windows Style) */ +#ifdef Q_OS_WIN32 + Default = Windows +#else + Default = GNU +#endif +}; +} + +/** + * @brief The ArgumentStyle enum + */ +namespace ArgumentStyle +{ +enum Enum +{ + Space, /**< --option value */ + Equals, /**< --option=value */ + SpaceAndEquals, /**< --option[= ]value */ +#ifdef Q_OS_WIN32 + Default = Equals +#else + Default = SpaceAndEquals +#endif +}; +} + +/** + * @brief The ParsingError class + */ +class ParsingError : public std::runtime_error +{ +public: + ParsingError(const QString &what); +}; + +/** + * @brief The Parser class + */ +class Parser +{ +public: + /** + * @brief Parser constructor + * @param flagStyle the FlagStyle to use in this Parser + * @param argStyle the ArgumentStyle to use in this Parser + */ + Parser(FlagStyle::Enum flagStyle = FlagStyle::Default, + ArgumentStyle::Enum argStyle = ArgumentStyle::Default); + + /** + * @brief set the flag style + * @param style + */ + void setFlagStyle(FlagStyle::Enum style); + + /** + * @brief get the flag style + * @return + */ + FlagStyle::Enum flagStyle(); + + /** + * @brief set the argument style + * @param style + */ + void setArgumentStyle(ArgumentStyle::Enum style); + + /** + * @brief get the argument style + * @return + */ + ArgumentStyle::Enum argumentStyle(); + + /** + * @brief define a boolean switch + * @param name the parameter name + * @param def the default value + */ + void addSwitch(QString name, bool def = false); + + /** + * @brief define an option that takes an additional argument + * @param name the parameter name + * @param def the default value + */ + void addOption(QString name, QVariant def = QVariant()); + + /** + * @brief define a positional argument + * @param name the parameter name + * @param required wether this argument is required + * @param def the default value + */ + void addArgument(QString name, bool required = true, QVariant def = QVariant()); + + /** + * @brief adds a flag to an existing parameter + * @param name the (existing) parameter name + * @param flag the flag character + * @see addSwitch addArgument addOption + * Note: any one parameter can only have one flag + */ + void addShortOpt(QString name, QChar flag); + + /** + * @brief adds documentation to a Parameter + * @param name the parameter name + * @param metavar a string to be displayed as placeholder for the value + * @param doc a QString containing the documentation + * Note: on positional arguments, metavar replaces the name as displayed. + * on options , metavar replaces the value placeholder + */ + void addDocumentation(QString name, QString doc, QString metavar = QString()); + + /** + * @brief generate a help message + * @param progName the program name to use in the help message + * @param helpIndent how much the parameter documentation should be indented + * @param flagsInUsage whether we should use flags instead of options in the usage + * @return a help message + */ + QString compileHelp(QString progName, int helpIndent = 22, bool flagsInUsage = true); + + /** + * @brief generate a short usage message + * @param progName the program name to use in the usage message + * @param useFlags whether we should use flags instead of options + * @return a usage message + */ + QString compileUsage(QString progName, bool useFlags = true); + + /** + * @brief parse + * @param argv a QStringList containing the program ARGV + * @return a QHash mapping argument names to their values + */ + QHash parse(QStringList argv); + + /** + * @brief clear all definitions + */ + void clear(); + + ~Parser(); + +private: + FlagStyle::Enum m_flagStyle; + ArgumentStyle::Enum m_argStyle; + + enum OptionType + { + otSwitch, + otOption + }; + + // Important: the common part MUST BE COMMON ON ALL THREE structs + struct CommonDef + { + QString name; + QString doc; + QString metavar; + QVariant def; + }; + + struct OptionDef + { + // common + QString name; + QString doc; + QString metavar; + QVariant def; + // option + OptionType type; + QChar flag; + }; + + struct PositionalDef + { + // common + QString name; + QString doc; + QString metavar; + QVariant def; + // positional + bool required; + }; + + QHash m_options; + QHash m_flags; + QHash m_params; + QList m_positionals; + QList m_optionList; + + void getPrefix(QString &opt, QString &flag); +}; +} diff --git a/ultimmc/launcher/DefaultVariable.h b/ultimmc/launcher/DefaultVariable.h new file mode 100644 index 0000000..5c069bd --- /dev/null +++ b/ultimmc/launcher/DefaultVariable.h @@ -0,0 +1,35 @@ +#pragma once + +template +class DefaultVariable +{ +public: + DefaultVariable(const T & value) + { + defaultValue = value; + } + DefaultVariable & operator =(const T & value) + { + currentValue = value; + is_default = currentValue == defaultValue; + is_explicit = true; + return *this; + } + operator const T &() const + { + return is_default ? defaultValue : currentValue; + } + bool isDefault() const + { + return is_default; + } + bool isExplicit() const + { + return is_explicit; + } +private: + T currentValue; + T defaultValue; + bool is_default = true; + bool is_explicit = false; +}; diff --git a/ultimmc/launcher/DesktopServices.cpp b/ultimmc/launcher/DesktopServices.cpp new file mode 100644 index 0000000..dcc1b0c --- /dev/null +++ b/ultimmc/launcher/DesktopServices.cpp @@ -0,0 +1,149 @@ +#include "DesktopServices.h" +#include +#include +#include +#include + +/** + * This shouldn't exist, but until QTBUG-9328 and other unreported bugs are fixed, it needs to be a thing. + */ +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + +#include +#include +#include +#include + +template +bool IndirectOpen(T callable, qint64 *pid_forked = nullptr) +{ + auto pid = fork(); + if(pid_forked) + { + if(pid > 0) + *pid_forked = pid; + else + *pid_forked = 0; + } + if(pid == -1) + { + qWarning() << "IndirectOpen failed to fork: " << errno; + return false; + } + // child - do the stuff + if(pid == 0) + { + // unset all this garbage so it doesn't get passed to the child process + qunsetenv("LD_PRELOAD"); + qunsetenv("LD_LIBRARY_PATH"); + qunsetenv("LD_DEBUG"); + qunsetenv("QT_PLUGIN_PATH"); + qunsetenv("QT_FONTPATH"); + + // open the URL + auto status = callable(); + + // detach from the parent process group. + setsid(); + + // die. now. do not clean up anything, it would just hang forever. + _exit(status ? 0 : 1); + } + else + { + //parent - assume it worked. + int status; + while (waitpid(pid, &status, 0)) + { + if(WIFEXITED(status)) + { + return WEXITSTATUS(status) == 0; + } + if(WIFSIGNALED(status)) + { + return false; + } + } + return true; + } +} +#endif + +namespace DesktopServices { +bool openDirectory(const QString &path, bool ensureExists) +{ + qDebug() << "Opening directory" << path; + QDir parentPath; + QDir dir(path); + if (!dir.exists()) + { + parentPath.mkpath(dir.absolutePath()); + } + auto f = [&]() + { + return QDesktopServices::openUrl(QUrl::fromLocalFile(dir.absolutePath())); + }; +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + return IndirectOpen(f); +#else + return f(); +#endif +} + +bool openFile(const QString &path) +{ + qDebug() << "Opening file" << path; + auto f = [&]() + { + return QDesktopServices::openUrl(QUrl::fromLocalFile(path)); + }; +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + return IndirectOpen(f); +#else + return f(); +#endif +} + +bool openFile(const QString &application, const QString &path, const QString &workingDirectory, qint64 *pid) +{ + qDebug() << "Opening file" << path << "using" << application; +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + // FIXME: the pid here is fake. So if something depends on it, it will likely misbehave + return IndirectOpen([&]() + { + return QProcess::startDetached(application, QStringList() << path, workingDirectory); + }, pid); +#else + return QProcess::startDetached(application, QStringList() << path, workingDirectory, pid); +#endif +} + +bool run(const QString &application, const QStringList &args, const QString &workingDirectory, qint64 *pid) +{ + qDebug() << "Running" << application << "with args" << args.join(' '); +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + // FIXME: the pid here is fake. So if something depends on it, it will likely misbehave + return IndirectOpen([&]() + { + return QProcess::startDetached(application, args, workingDirectory); + }, pid); +#else + return QProcess::startDetached(application, args, workingDirectory, pid); +#endif +} + +bool openUrl(const QUrl &url) +{ + qDebug() << "Opening URL" << url.toString(); + auto f = [&]() + { + return QDesktopServices::openUrl(url); + }; +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + return IndirectOpen(f); +#else + return f(); +#endif +} + +} diff --git a/ultimmc/launcher/DesktopServices.h b/ultimmc/launcher/DesktopServices.h new file mode 100644 index 0000000..1c081da --- /dev/null +++ b/ultimmc/launcher/DesktopServices.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +/** + * This wraps around QDesktopServices and adds workarounds where needed + * Use this instead of QDesktopServices! + */ +namespace DesktopServices +{ + /** + * Open a file in whatever application is applicable + */ + bool openFile(const QString &path); + + /** + * Open a file in the specified application + */ + bool openFile(const QString &application, const QString &path, const QString & workingDirectory = QString(), qint64 *pid = 0); + + /** + * Run an application + */ + bool run(const QString &application,const QStringList &args, const QString & workingDirectory = QString(), qint64 *pid = 0); + + /** + * Open a directory + */ + bool openDirectory(const QString &path, bool ensureExists = false); + + /** + * Open the URL, most likely in a browser. Maybe. + */ + bool openUrl(const QUrl &url); +} diff --git a/ultimmc/launcher/Exception.h b/ultimmc/launcher/Exception.h new file mode 100644 index 0000000..fe0b86b --- /dev/null +++ b/ultimmc/launcher/Exception.h @@ -0,0 +1,32 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#pragma once + +#include +#include +#include + +class Exception : public std::exception +{ +public: + Exception(const QString &message) : std::exception(), m_message(message) + { + qCritical() << "Exception:" << message; + } + Exception(const Exception &other) + : std::exception(), m_message(other.cause()) + { + } + virtual ~Exception() noexcept {} + const char *what() const noexcept + { + return m_message.toLatin1().constData(); + } + QString cause() const + { + return m_message; + } + +private: + QString m_message; +}; diff --git a/ultimmc/launcher/ExponentialSeries.h b/ultimmc/launcher/ExponentialSeries.h new file mode 100644 index 0000000..a9487f0 --- /dev/null +++ b/ultimmc/launcher/ExponentialSeries.h @@ -0,0 +1,43 @@ + +#pragma once + +template +inline void clamp(T& current, T min, T max) +{ + if (current < min) + { + current = min; + } + else if(current > max) + { + current = max; + } +} + +// List of numbers from min to max. Next is exponent times bigger than previous. + +class ExponentialSeries +{ +public: + ExponentialSeries(unsigned min, unsigned max, unsigned exponent = 2) + { + m_current = m_min = min; + m_max = max; + m_exponent = exponent; + } + void reset() + { + m_current = m_min; + } + unsigned operator()() + { + unsigned retval = m_current; + m_current *= m_exponent; + clamp(m_current, m_min, m_max); + return retval; + } + unsigned m_current; + unsigned m_min; + unsigned m_max; + unsigned m_exponent; +}; diff --git a/ultimmc/launcher/FileSystem.cpp b/ultimmc/launcher/FileSystem.cpp new file mode 100644 index 0000000..6de20de --- /dev/null +++ b/ultimmc/launcher/FileSystem.cpp @@ -0,0 +1,457 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#include "FileSystem.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined Q_OS_WIN32 + #include + #include + #include + #include + #include + #include + #include + #include + #include +#else + #include +#endif + +namespace FS { + +void ensureExists(const QDir &dir) +{ + if (!QDir().mkpath(dir.absolutePath())) + { + throw FileSystemException("Unable to create folder " + dir.dirName() + " (" + + dir.absolutePath() + ")"); + } +} + +void write(const QString &filename, const QByteArray &data) +{ + ensureExists(QFileInfo(filename).dir()); + QSaveFile file(filename); + if (!file.open(QSaveFile::WriteOnly)) + { + throw FileSystemException("Couldn't open " + filename + " for writing: " + + file.errorString()); + } + if (data.size() != file.write(data)) + { + throw FileSystemException("Error writing data to " + filename + ": " + + file.errorString()); + } + if (!file.commit()) + { + throw FileSystemException("Error while committing data to " + filename + ": " + + file.errorString()); + } +} + +QByteArray read(const QString &filename) +{ + QFile file(filename); + if (!file.open(QFile::ReadOnly)) + { + throw FileSystemException("Unable to open " + filename + " for reading: " + + file.errorString()); + } + const qint64 size = file.size(); + QByteArray data(int(size), 0); + const qint64 ret = file.read(data.data(), size); + if (ret == -1 || ret != size) + { + throw FileSystemException("Error reading data from " + filename + ": " + + file.errorString()); + } + return data; +} + +bool updateTimestamp(const QString& filename) +{ +#ifdef Q_OS_WIN32 + std::wstring filename_utf_16 = filename.toStdWString(); + return (_wutime64(filename_utf_16.c_str(), nullptr) == 0); +#else + QByteArray filenameBA = QFile::encodeName(filename); + return (utime(filenameBA.data(), nullptr) == 0); +#endif +} + +bool ensureFilePathExists(QString filenamepath) +{ + QFileInfo a(filenamepath); + QDir dir; + QString ensuredPath = a.path(); + bool success = dir.mkpath(ensuredPath); + return success; +} + +bool ensureFolderPathExists(QString foldernamepath) +{ + QFileInfo a(foldernamepath); + QDir dir; + QString ensuredPath = a.filePath(); + bool success = dir.mkpath(ensuredPath); + return success; +} + +bool copy::operator()(const QString &offset) +{ + //NOTE always deep copy on windows. the alternatives are too messy. + #if defined Q_OS_WIN32 + m_followSymlinks = true; + #endif + + auto src = PathCombine(m_src.absolutePath(), offset); + auto dst = PathCombine(m_dst.absolutePath(), offset); + + QFileInfo currentSrc(src); + if (!currentSrc.exists()) + return false; + + if(!m_followSymlinks && currentSrc.isSymLink()) + { + qDebug() << "creating symlink" << src << " - " << dst; + if (!ensureFilePathExists(dst)) + { + qWarning() << "Cannot create path!"; + return false; + } + return QFile::link(currentSrc.symLinkTarget(), dst); + } + else if(currentSrc.isFile()) + { + qDebug() << "copying file" << src << " - " << dst; + if (!ensureFilePathExists(dst)) + { + qWarning() << "Cannot create path!"; + return false; + } + return QFile::copy(src, dst); + } + else if(currentSrc.isDir()) + { + qDebug() << "recursing" << offset; + if (!ensureFolderPathExists(dst)) + { + qWarning() << "Cannot create path!"; + return false; + } + QDir currentDir(src); + for(auto & f : currentDir.entryList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden | QDir::System)) + { + auto inner_offset = PathCombine(offset, f); + // ignore and skip stuff that matches the blacklist. + if(m_blacklist && m_blacklist->matches(inner_offset)) + { + continue; + } + if(!operator()(inner_offset)) + { + qWarning() << "Failed to copy" << inner_offset; + return false; + } + } + } + else + { + qCritical() << "Copy ERROR: Unknown filesystem object:" << src; + return false; + } + return true; +} + +bool deletePath(QString path) +{ + bool OK = true; + QFileInfo finfo(path); + if(finfo.isFile()) { + return QFile::remove(path); + } + + QDir dir(path); + + if (!dir.exists()) + { + return OK; + } + auto allEntries = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden | + QDir::AllDirs | QDir::Files, + QDir::DirsFirst); + + for(auto & info: allEntries) + { +#if defined Q_OS_WIN32 + QString nativePath = QDir::toNativeSeparators(info.absoluteFilePath()); + auto wString = nativePath.toStdWString(); + DWORD dwAttrs = GetFileAttributesW(wString.c_str()); + // Windows: check for junctions, reparse points and other nasty things of that sort + if(dwAttrs & FILE_ATTRIBUTE_REPARSE_POINT) + { + if (info.isFile()) + { + OK &= QFile::remove(info.absoluteFilePath()); + } + else if (info.isDir()) + { + OK &= dir.rmdir(info.absoluteFilePath()); + } + } +#else + // We do not trust Qt with reparse points, but do trust it with unix symlinks. + if(info.isSymLink()) + { + OK &= QFile::remove(info.absoluteFilePath()); + } +#endif + else if (info.isDir()) + { + OK &= deletePath(info.absoluteFilePath()); + } + else if (info.isFile()) + { + OK &= QFile::remove(info.absoluteFilePath()); + } + else + { + OK = false; + qCritical() << "Delete ERROR: Unknown filesystem object:" << info.absoluteFilePath(); + } + } + OK &= dir.rmdir(dir.absolutePath()); + return OK; +} + + +QString PathCombine(const QString & path1, const QString & path2) +{ + if(!path1.size()) + return path2; + if(!path2.size()) + return path1; + return QDir::cleanPath(path1 + QDir::separator() + path2); +} + +QString PathCombine(const QString & path1, const QString & path2, const QString & path3) +{ + return PathCombine(PathCombine(path1, path2), path3); +} + +QString PathCombine(const QString & path1, const QString & path2, const QString & path3, const QString & path4) +{ + return PathCombine(PathCombine(path1, path2, path3), path4); +} + +QString AbsolutePath(QString path) +{ + return QFileInfo(path).absolutePath(); +} + +QString ResolveExecutable(QString path) +{ + if (path.isEmpty()) + { + return QString(); + } + if(!path.contains('/')) + { + path = QStandardPaths::findExecutable(path); + } + QFileInfo pathInfo(path); + if(!pathInfo.exists() || !pathInfo.isExecutable()) + { + return QString(); + } + return pathInfo.absoluteFilePath(); +} + +/** + * Normalize path + * + * Any paths inside the current folder will be normalized to relative paths (to current) + * Other paths will be made absolute + */ +QString NormalizePath(QString path) +{ + QDir a = QDir::currentPath(); + QString currentAbsolute = a.absolutePath(); + + QDir b(path); + QString newAbsolute = b.absolutePath(); + + if (newAbsolute.startsWith(currentAbsolute)) + { + return a.relativeFilePath(newAbsolute); + } + else + { + return newAbsolute; + } +} + +QString badFilenameChars = "\"\\/?<>:;*|!+\r\n"; + +QString RemoveInvalidFilenameChars(QString string, QChar replaceWith) +{ + for (int i = 0; i < string.length(); i++) + { + if (badFilenameChars.contains(string[i])) + { + string[i] = replaceWith; + } + } + return string; +} + +QString DirNameFromString(QString string, QString inDir) +{ + int num = 0; + QString baseName = RemoveInvalidFilenameChars(string, '-'); + QString dirName; + do + { + if(num == 0) + { + dirName = baseName; + } + else + { + dirName = baseName + QString::number(num);; + } + + // If it's over 9000 + if (num > 9000) + return ""; + num++; + } while (QFileInfo(PathCombine(inDir, dirName)).exists()); + return dirName; +} + +// Does the folder path contain any '!'? If yes, return true, otherwise false. +// (This is a problem for Java) +bool checkProblemticPathJava(QDir folder) +{ + QString pathfoldername = folder.absolutePath(); + return pathfoldername.contains("!", Qt::CaseInsensitive); +} + +// Win32 crap +#if defined Q_OS_WIN + +bool called_coinit = false; + +HRESULT CreateLink(LPCSTR linkPath, LPCSTR targetPath, LPCSTR args) +{ + HRESULT hres; + + if (!called_coinit) + { + hres = CoInitialize(NULL); + called_coinit = true; + + if (!SUCCEEDED(hres)) + { + qWarning("Failed to initialize COM. Error 0x%08lX", hres); + return hres; + } + } + + IShellLink *link; + hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, + (LPVOID *)&link); + + if (SUCCEEDED(hres)) + { + IPersistFile *persistFile; + + link->SetPath(targetPath); + link->SetArguments(args); + + hres = link->QueryInterface(IID_IPersistFile, (LPVOID *)&persistFile); + if (SUCCEEDED(hres)) + { + WCHAR wstr[MAX_PATH]; + + MultiByteToWideChar(CP_ACP, 0, linkPath, -1, wstr, MAX_PATH); + + hres = persistFile->Save(wstr, TRUE); + persistFile->Release(); + } + link->Release(); + } + return hres; +} + +#endif + +QString getDesktopDir() +{ + return QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); +} + +// Cross-platform Shortcut creation +bool createShortCut(QString location, QString dest, QStringList args, QString name, + QString icon) +{ +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + location = PathCombine(location, name + ".desktop"); + + QFile f(location); + f.open(QIODevice::WriteOnly | QIODevice::Text); + QTextStream stream(&f); + + QString argstring; + if (!args.empty()) + argstring = " '" + args.join("' '") + "'"; + + stream << "[Desktop Entry]" + << "\n"; + stream << "Type=Application" + << "\n"; + stream << "TryExec=" << dest.toLocal8Bit() << "\n"; + stream << "Exec=" << dest.toLocal8Bit() << argstring.toLocal8Bit() << "\n"; + stream << "Name=" << name.toLocal8Bit() << "\n"; + stream << "Icon=" << icon.toLocal8Bit() << "\n"; + + stream.flush(); + f.close(); + + f.setPermissions(f.permissions() | QFileDevice::ExeOwner | QFileDevice::ExeGroup | + QFileDevice::ExeOther); + + return true; +#elif defined Q_OS_WIN + // TODO: Fix + // QFile file(PathCombine(location, name + ".lnk")); + // WCHAR *file_w; + // WCHAR *dest_w; + // WCHAR *args_w; + // file.fileName().toWCharArray(file_w); + // dest.toWCharArray(dest_w); + + // QString argStr; + // for (int i = 0; i < args.count(); i++) + // { + // argStr.append(args[i]); + // argStr.append(" "); + // } + // argStr.toWCharArray(args_w); + + // return SUCCEEDED(CreateLink(file_w, dest_w, args_w)); + return false; +#else + qWarning("Desktop Shortcuts not supported on your platform!"); + return false; +#endif +} +} diff --git a/ultimmc/launcher/FileSystem.h b/ultimmc/launcher/FileSystem.h new file mode 100644 index 0000000..8f6e8b4 --- /dev/null +++ b/ultimmc/launcher/FileSystem.h @@ -0,0 +1,127 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#pragma once + +#include "Exception.h" +#include "pathmatcher/IPathMatcher.h" + +#include +#include + +namespace FS +{ + +class FileSystemException : public ::Exception +{ +public: + FileSystemException(const QString &message) : Exception(message) {} +}; + +/** + * write data to a file safely + */ +void write(const QString &filename, const QByteArray &data); + +/** + * read data from a file safely\ + */ +QByteArray read(const QString &filename); + +/** + * Update the last changed timestamp of an existing file + */ +bool updateTimestamp(const QString & filename); + +/** + * Creates all the folders in a path for the specified path + * last segment of the path is treated as a file name and is ignored! + */ +bool ensureFilePathExists(QString filenamepath); + +/** + * Creates all the folders in a path for the specified path + * last segment of the path is treated as a folder name and is created! + */ +bool ensureFolderPathExists(QString filenamepath); + +class copy +{ +public: + copy(const QString & src, const QString & dst) + { + m_src = src; + m_dst = dst; + } + copy & followSymlinks(const bool follow) + { + m_followSymlinks = follow; + return *this; + } + copy & blacklist(const IPathMatcher * filter) + { + m_blacklist = filter; + return *this; + } + bool operator()() + { + return operator()(QString()); + } + +private: + bool operator()(const QString &offset); + +private: + bool m_followSymlinks = true; + const IPathMatcher * m_blacklist = nullptr; + QDir m_src; + QDir m_dst; +}; + +/** + * Delete a folder recursively + */ +bool deletePath(QString path); + +QString PathCombine(const QString &path1, const QString &path2); +QString PathCombine(const QString &path1, const QString &path2, const QString &path3); +QString PathCombine(const QString &path1, const QString &path2, const QString &path3, const QString &path4); + +QString AbsolutePath(QString path); + +/** + * Resolve an executable + * + * Will resolve: + * single executable (by name) + * relative path + * absolute path + * + * @return absolute path to executable or null string + */ +QString ResolveExecutable(QString path); + +/** + * Normalize path + * + * Any paths inside the current directory will be normalized to relative paths (to current) + * Other paths will be made absolute + * + * Returns false if the path logic somehow filed (and normalizedPath in invalid) + */ +QString NormalizePath(QString path); + +QString RemoveInvalidFilenameChars(QString string, QChar replaceWith = '-'); + +QString DirNameFromString(QString string, QString inDir = "."); + +/// Checks if the a given Path contains "!" +bool checkProblemticPathJava(QDir folder); + +// Get the Directory representing the User's Desktop +QString getDesktopDir(); + +// Create a shortcut at *location*, pointing to *dest* called with the arguments *args* +// call it *name* and assign it the icon *icon* +// return true if operation succeeded +bool createShortCut(QString location, QString dest, QStringList args, QString name, QString iconLocation); +} diff --git a/ultimmc/launcher/FileSystem_test.cpp b/ultimmc/launcher/FileSystem_test.cpp new file mode 100644 index 0000000..90ddc49 --- /dev/null +++ b/ultimmc/launcher/FileSystem_test.cpp @@ -0,0 +1,164 @@ +#include +#include +#include +#include "TestUtil.h" + +#include "FileSystem.h" + +class FileSystemTest : public QObject +{ + Q_OBJECT + + const QString bothSlash = "/foo/"; + const QString trailingSlash = "foo/"; + const QString leadingSlash = "/foo"; + +private +slots: + void test_pathCombine() + { + QCOMPARE(QString("/foo/foo"), FS::PathCombine(bothSlash, bothSlash)); + QCOMPARE(QString("foo/foo"), FS::PathCombine(trailingSlash, trailingSlash)); + QCOMPARE(QString("/foo/foo"), FS::PathCombine(leadingSlash, leadingSlash)); + + QCOMPARE(QString("/foo/foo/foo"), FS::PathCombine(bothSlash, bothSlash, bothSlash)); + QCOMPARE(QString("foo/foo/foo"), FS::PathCombine(trailingSlash, trailingSlash, trailingSlash)); + QCOMPARE(QString("/foo/foo/foo"), FS::PathCombine(leadingSlash, leadingSlash, leadingSlash)); + } + + void test_PathCombine1_data() + { + QTest::addColumn("result"); + QTest::addColumn("path1"); + QTest::addColumn("path2"); + + QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc/def" << "ghi/jkl"; + QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/def/" << "ghi/jkl"; +#if defined(Q_OS_WIN) + QTest::newRow("win native, from C:") << "C:/abc" << "C:" << "abc"; + QTest::newRow("win native 1") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def" << "ghi\\jkl"; + QTest::newRow("win native 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\def\\" << "ghi\\jkl"; +#endif + } + + void test_PathCombine1() + { + QFETCH(QString, result); + QFETCH(QString, path1); + QFETCH(QString, path2); + + QCOMPARE(FS::PathCombine(path1, path2), result); + } + + void test_PathCombine2_data() + { + QTest::addColumn("result"); + QTest::addColumn("path1"); + QTest::addColumn("path2"); + QTest::addColumn("path3"); + + QTest::newRow("qt 1") << "/abc/def/ghi/jkl" << "/abc" << "def" << "ghi/jkl"; + QTest::newRow("qt 2") << "/abc/def/ghi/jkl" << "/abc/" << "def" << "ghi/jkl"; + QTest::newRow("qt 3") << "/abc/def/ghi/jkl" << "/abc" << "def/" << "ghi/jkl"; + QTest::newRow("qt 4") << "/abc/def/ghi/jkl" << "/abc/" << "def/" << "ghi/jkl"; +#if defined(Q_OS_WIN) + QTest::newRow("win 1") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def" << "ghi\\jkl"; + QTest::newRow("win 2") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl"; + QTest::newRow("win 3") << "C:/abc/def/ghi/jkl" << "C:\\abc" << "def\\" << "ghi\\jkl"; + QTest::newRow("win 4") << "C:/abc/def/ghi/jkl" << "C:\\abc\\" << "def" << "ghi\\jkl"; +#endif + } + + void test_PathCombine2() + { + QFETCH(QString, result); + QFETCH(QString, path1); + QFETCH(QString, path2); + QFETCH(QString, path3); + + QCOMPARE(FS::PathCombine(path1, path2, path3), result); + } + + void test_copy() + { + QString folder = QFINDTESTDATA("data/test_folder"); + auto f = [&folder]() + { + QTemporaryDir tempDir; + tempDir.setAutoRemove(true); + qDebug() << "From:" << folder << "To:" << tempDir.path(); + + QDir target_dir(FS::PathCombine(tempDir.path(), "test_folder")); + qDebug() << tempDir.path(); + qDebug() << target_dir.path(); + FS::copy c(folder, target_dir.path()); + c(); + + for(auto entry: target_dir.entryList()) + { + qDebug() << entry; + } + QVERIFY(target_dir.entryList().contains("pack.mcmeta")); + QVERIFY(target_dir.entryList().contains("assets")); + }; + + // first try variant without trailing / + QVERIFY(!folder.endsWith('/')); + f(); + + // then variant with trailing / + folder.append('/'); + QVERIFY(folder.endsWith('/')); + f(); + } + + void test_getDesktop() + { + QCOMPARE(FS::getDesktopDir(), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); + } + +// this is only valid on linux +// FIXME: implement on windows, OSX, then test. +#if defined(Q_OS_LINUX) + void test_createShortcut_data() + { + QTest::addColumn("location"); + QTest::addColumn("dest"); + QTest::addColumn("args"); + QTest::addColumn("name"); + QTest::addColumn("iconLocation"); + QTest::addColumn("result"); + + QTest::newRow("unix") << QDir::currentPath() + << "asdfDest" + << (QStringList() << "arg1" << "arg2") + << "asdf" + << QString() + #if defined(Q_OS_LINUX) + << GET_TEST_FILE("data/FileSystem-test_createShortcut-unix") + #elif defined(Q_OS_WIN) + << QByteArray() + #endif + ; + } + + void test_createShortcut() + { + QFETCH(QString, location); + QFETCH(QString, dest); + QFETCH(QStringList, args); + QFETCH(QString, name); + QFETCH(QString, iconLocation); + QFETCH(QByteArray, result); + + QVERIFY(FS::createShortCut(location, dest, args, name, iconLocation)); + QCOMPARE(QString::fromLocal8Bit(TestsInternal::readFile(location + QDir::separator() + name + ".desktop")), QString::fromLocal8Bit(result)); + + //QDir().remove(location); + } +#endif +}; + +QTEST_GUILESS_MAIN(FileSystemTest) + +#include "FileSystem_test.moc" diff --git a/ultimmc/launcher/Filter.cpp b/ultimmc/launcher/Filter.cpp new file mode 100644 index 0000000..c65ca0c --- /dev/null +++ b/ultimmc/launcher/Filter.cpp @@ -0,0 +1,31 @@ +#include "Filter.h" + +Filter::~Filter(){} + +ContainsFilter::ContainsFilter(const QString& pattern) : pattern(pattern){} +ContainsFilter::~ContainsFilter(){} +bool ContainsFilter::accepts(const QString& value) +{ + return value.contains(pattern); +} + +ExactFilter::ExactFilter(const QString& pattern) : pattern(pattern){} +ExactFilter::~ExactFilter(){} +bool ExactFilter::accepts(const QString& value) +{ + return value == pattern; +} + +RegexpFilter::RegexpFilter(const QString& regexp, bool invert) + :invert(invert) +{ + pattern.setPattern(regexp); + pattern.optimize(); +} +RegexpFilter::~RegexpFilter(){} +bool RegexpFilter::accepts(const QString& value) +{ + auto match = pattern.match(value); + bool matched = match.hasMatch(); + return invert ? (!matched) : (matched); +} diff --git a/ultimmc/launcher/Filter.h b/ultimmc/launcher/Filter.h new file mode 100644 index 0000000..b55067a --- /dev/null +++ b/ultimmc/launcher/Filter.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include + +class Filter +{ +public: + virtual ~Filter(); + virtual bool accepts(const QString & value) = 0; +}; + +class ContainsFilter: public Filter +{ +public: + ContainsFilter(const QString &pattern); + virtual ~ContainsFilter(); + bool accepts(const QString & value) override; +private: + QString pattern; +}; + +class ExactFilter: public Filter +{ +public: + ExactFilter(const QString &pattern); + virtual ~ExactFilter(); + bool accepts(const QString & value) override; +private: + QString pattern; +}; + +class RegexpFilter: public Filter +{ +public: + RegexpFilter(const QString ®exp, bool invert); + virtual ~RegexpFilter(); + bool accepts(const QString & value) override; +private: + QRegularExpression pattern; + bool invert = false; +}; diff --git a/ultimmc/launcher/GZip.cpp b/ultimmc/launcher/GZip.cpp new file mode 100644 index 0000000..0368c32 --- /dev/null +++ b/ultimmc/launcher/GZip.cpp @@ -0,0 +1,115 @@ +#include "GZip.h" +#include +#include + +bool GZip::unzip(const QByteArray &compressedBytes, QByteArray &uncompressedBytes) +{ + if (compressedBytes.size() == 0) + { + uncompressedBytes = compressedBytes; + return true; + } + + unsigned uncompLength = compressedBytes.size(); + uncompressedBytes.clear(); + uncompressedBytes.resize(uncompLength); + + z_stream strm; + memset(&strm, 0, sizeof(strm)); + strm.next_in = (Bytef *)compressedBytes.data(); + strm.avail_in = compressedBytes.size(); + + bool done = false; + + if (inflateInit2(&strm, (16 + MAX_WBITS)) != Z_OK) + { + return false; + } + + int err = Z_OK; + + while (!done) + { + // If our output buffer is too small + if (strm.total_out >= uncompLength) + { + uncompressedBytes.resize(uncompLength * 2); + uncompLength *= 2; + } + + strm.next_out = (Bytef *)(uncompressedBytes.data() + strm.total_out); + strm.avail_out = uncompLength - strm.total_out; + + // Inflate another chunk. + err = inflate(&strm, Z_SYNC_FLUSH); + if (err == Z_STREAM_END) + done = true; + else if (err != Z_OK) + { + break; + } + } + + if (inflateEnd(&strm) != Z_OK || !done) + { + return false; + } + + uncompressedBytes.resize(strm.total_out); + return true; +} + +bool GZip::zip(const QByteArray &uncompressedBytes, QByteArray &compressedBytes) +{ + if (uncompressedBytes.size() == 0) + { + compressedBytes = uncompressedBytes; + return true; + } + + unsigned compLength = std::min(uncompressedBytes.size(), 16); + compressedBytes.clear(); + compressedBytes.resize(compLength); + + z_stream zs; + memset(&zs, 0, sizeof(zs)); + + if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, (16 + MAX_WBITS), 8, Z_DEFAULT_STRATEGY) != Z_OK) + { + return false; + } + + zs.next_in = (Bytef*)uncompressedBytes.data(); + zs.avail_in = uncompressedBytes.size(); + + int ret; + compressedBytes.resize(uncompressedBytes.size()); + + unsigned offset = 0; + unsigned temp = 0; + do + { + auto remaining = compressedBytes.size() - offset; + if(remaining < 1) + { + compressedBytes.resize(compressedBytes.size() * 2); + } + zs.next_out = (Bytef *) (compressedBytes.data() + offset); + temp = zs.avail_out = compressedBytes.size() - offset; + ret = deflate(&zs, Z_FINISH); + offset += temp - zs.avail_out; + } while (ret == Z_OK); + + compressedBytes.resize(offset); + + if (deflateEnd(&zs) != Z_OK) + { + return false; + } + + if (ret != Z_STREAM_END) + { + return false; + } + return true; +} \ No newline at end of file diff --git a/ultimmc/launcher/GZip.h b/ultimmc/launcher/GZip.h new file mode 100644 index 0000000..7d4b1c3 --- /dev/null +++ b/ultimmc/launcher/GZip.h @@ -0,0 +1,10 @@ +#pragma once +#include + +class GZip +{ +public: + static bool unzip(const QByteArray &compressedBytes, QByteArray &uncompressedBytes); + static bool zip(const QByteArray &uncompressedBytes, QByteArray &compressedBytes); +}; + diff --git a/ultimmc/launcher/GZip_test.cpp b/ultimmc/launcher/GZip_test.cpp new file mode 100644 index 0000000..3f4d181 --- /dev/null +++ b/ultimmc/launcher/GZip_test.cpp @@ -0,0 +1,57 @@ +#include +#include "TestUtil.h" + +#include "GZip.h" +#include + +void fib(int &prev, int &cur) +{ + auto ret = prev + cur; + prev = cur; + cur = ret; +} + +class GZipTest : public QObject +{ + Q_OBJECT +private +slots: + + void test_Through() + { + // test up to 10 MB + static const int size = 10 * 1024 * 1024; + QByteArray random; + QByteArray compressed; + QByteArray decompressed; + std::default_random_engine eng((std::random_device())()); + std::uniform_int_distribution idis(0, std::numeric_limits::max()); + + // initialize random buffer + for(int i = 0; i < size; i++) + { + random.append((char)idis(eng)); + } + + // initialize fibonacci + int prev = 1; + int cur = 1; + + // test if fibonacci long random buffers pass through GZip + do + { + QByteArray copy = random; + copy.resize(cur); + compressed.clear(); + decompressed.clear(); + QVERIFY(GZip::zip(copy, compressed)); + QVERIFY(GZip::unzip(compressed, decompressed)); + QCOMPARE(decompressed, copy); + fib(prev, cur); + } while (cur < size); + } +}; + +QTEST_GUILESS_MAIN(GZipTest) + +#include "GZip_test.moc" diff --git a/ultimmc/launcher/HoeDown.h b/ultimmc/launcher/HoeDown.h new file mode 100644 index 0000000..a8b831a --- /dev/null +++ b/ultimmc/launcher/HoeDown.h @@ -0,0 +1,76 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +/** + * hoedown wrapper, because dealing with resource lifetime in C is stupid + */ +class HoeDown +{ +public: + class buffer + { + public: + buffer(size_t unit = 4096) + { + buf = hoedown_buffer_new(unit); + } + ~buffer() + { + hoedown_buffer_free(buf); + } + const char * cstr() + { + return hoedown_buffer_cstr(buf); + } + void put(QByteArray input) + { + hoedown_buffer_put(buf, (uint8_t *) input.data(), input.size()); + } + const uint8_t * data() const + { + return buf->data; + } + size_t size() const + { + return buf->size; + } + hoedown_buffer * buf; + } ib, ob; + HoeDown() + { + renderer = hoedown_html_renderer_new((hoedown_html_flags) 0,0); + document = hoedown_document_new(renderer, (hoedown_extensions) HOEDOWN_EXT_TABLES, 8); + } + ~HoeDown() + { + hoedown_document_free(document); + hoedown_html_renderer_free(renderer); + } + QString process(QByteArray input) + { + ib.put(input); + hoedown_document_render(document, ob.buf, ib.data(), ib.size()); + return ob.cstr(); + } +private: + hoedown_document * document; + hoedown_renderer * renderer; +}; diff --git a/ultimmc/launcher/InstanceCopyTask.cpp b/ultimmc/launcher/InstanceCopyTask.cpp new file mode 100644 index 0000000..35adeaf --- /dev/null +++ b/ultimmc/launcher/InstanceCopyTask.cpp @@ -0,0 +1,60 @@ +#include "InstanceCopyTask.h" +#include "settings/INISettingsObject.h" +#include "FileSystem.h" +#include "NullInstance.h" +#include "pathmatcher/RegexpMatcher.h" +#include + +InstanceCopyTask::InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime) +{ + m_origInstance = origInstance; + m_keepPlaytime = keepPlaytime; + + if(!copySaves) + { + // FIXME: get this from the original instance type... + auto matcherReal = new RegexpMatcher("[.]?minecraft/saves"); + matcherReal->caseSensitive(false); + m_matcher.reset(matcherReal); + } +} + +void InstanceCopyTask::executeTask() +{ + setStatus(tr("Copying instance %1").arg(m_origInstance->name())); + + FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); + folderCopy.followSymlinks(false).blacklist(m_matcher.get()); + + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), folderCopy); + connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &InstanceCopyTask::copyFinished); + connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &InstanceCopyTask::copyAborted); + m_copyFutureWatcher.setFuture(m_copyFuture); +} + +void InstanceCopyTask::copyFinished() +{ + auto successful = m_copyFuture.result(); + if(!successful) + { + emitFailed(tr("Instance folder copy failed.")); + return; + } + // FIXME: shouldn't this be able to report errors? + auto instanceSettings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); + instanceSettings->registerSetting("InstanceType", "Legacy"); + + InstancePtr inst(new NullInstance(m_globalSettings, instanceSettings, m_stagingPath)); + inst->setName(m_instName); + inst->setIconKey(m_instIcon); + if(!m_keepPlaytime) { + inst->resetTimePlayed(); + } + emitSucceeded(); +} + +void InstanceCopyTask::copyAborted() +{ + emitFailed(tr("Instance folder copy has been aborted.")); + return; +} diff --git a/ultimmc/launcher/InstanceCopyTask.h b/ultimmc/launcher/InstanceCopyTask.h new file mode 100644 index 0000000..8290173 --- /dev/null +++ b/ultimmc/launcher/InstanceCopyTask.h @@ -0,0 +1,31 @@ +#pragma once + +#include "tasks/Task.h" +#include "net/NetJob.h" +#include +#include +#include +#include "settings/SettingsObject.h" +#include "BaseVersion.h" +#include "BaseInstance.h" +#include "InstanceTask.h" + +class InstanceCopyTask : public InstanceTask +{ + Q_OBJECT +public: + explicit InstanceCopyTask(InstancePtr origInstance, bool copySaves, bool keepPlaytime); + +protected: + //! Entry point for tasks. + virtual void executeTask() override; + void copyFinished(); + void copyAborted(); + +private: /* data */ + InstancePtr m_origInstance; + QFuture m_copyFuture; + QFutureWatcher m_copyFutureWatcher; + std::unique_ptr m_matcher; + bool m_keepPlaytime; +}; diff --git a/ultimmc/launcher/InstanceCreationTask.cpp b/ultimmc/launcher/InstanceCreationTask.cpp new file mode 100644 index 0000000..eafc512 --- /dev/null +++ b/ultimmc/launcher/InstanceCreationTask.cpp @@ -0,0 +1,31 @@ +#include "InstanceCreationTask.h" +#include "settings/INISettingsObject.h" +#include "FileSystem.h" + +//FIXME: remove this +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +InstanceCreationTask::InstanceCreationTask(BaseVersionPtr version) +{ + m_version = version; +} + +void InstanceCreationTask::executeTask() +{ + setStatus(tr("Creating instance from version %1").arg(m_version->name())); + { + auto instanceSettings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); + instanceSettings->suspendSave(); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + MinecraftInstance inst(m_globalSettings, instanceSettings, m_stagingPath); + auto components = inst.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_version->descriptor(), true); + inst.setName(m_instName); + inst.setIconKey(m_instIcon); + instanceSettings->resumeSave(); + } + emitSucceeded(); +} diff --git a/ultimmc/launcher/InstanceCreationTask.h b/ultimmc/launcher/InstanceCreationTask.h new file mode 100644 index 0000000..5499711 --- /dev/null +++ b/ultimmc/launcher/InstanceCreationTask.h @@ -0,0 +1,22 @@ +#pragma once + +#include "tasks/Task.h" +#include "net/NetJob.h" +#include +#include "settings/SettingsObject.h" +#include "BaseVersion.h" +#include "InstanceTask.h" + +class InstanceCreationTask : public InstanceTask +{ + Q_OBJECT +public: + explicit InstanceCreationTask(BaseVersionPtr version); + +protected: + //! Entry point for tasks. + virtual void executeTask() override; + +private: /* data */ + BaseVersionPtr m_version; +}; diff --git a/ultimmc/launcher/InstanceImportTask.cpp b/ultimmc/launcher/InstanceImportTask.cpp new file mode 100644 index 0000000..ca87b76 --- /dev/null +++ b/ultimmc/launcher/InstanceImportTask.cpp @@ -0,0 +1,489 @@ +/* Copyright 2013-2024 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "InstanceImportTask.h" +#include "BaseInstance.h" +#include "FileSystem.h" +#include "Application.h" +#include "MMCZip.h" +#include "NullInstance.h" +#include "settings/INISettingsObject.h" +#include "icons/IconUtils.h" +#include + +// FIXME: this does not belong here, it's Minecraft/Flame specific +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "Json.h" +#include +#include "modplatform/modrinth/ModrinthPackManifest.h" +#include "modplatform/technic/TechnicPackProcessor.h" + +#include "icons/IconList.h" +#include "Application.h" +#include "net/ChecksumValidator.h" + +#include +#include + +InstanceImportTask::InstanceImportTask(const QUrl sourceUrl) +{ + m_sourceUrl = sourceUrl; +} + +void InstanceImportTask::executeTask() +{ + if (m_sourceUrl.isLocalFile()) + { + m_archivePath = m_sourceUrl.toLocalFile(); + processZipPack(); + } + else + { + setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); + m_downloadRequired = true; + + const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); + auto entry = APPLICATION->metacache()->resolveEntry("general", path); + entry->setStale(true); + m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network()); + m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); + m_archivePath = entry->getFullPath(); + auto job = m_filesNetJob.get(); + connect(job, &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded); + connect(job, &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged); + connect(job, &NetJob::failed, this, &InstanceImportTask::downloadFailed); + m_filesNetJob->start(); + } +} + +void InstanceImportTask::downloadSucceeded() +{ + processZipPack(); + m_filesNetJob.reset(); +} + +void InstanceImportTask::downloadFailed(QString reason) +{ + emitFailed(reason); + m_filesNetJob.reset(); +} + +void InstanceImportTask::downloadProgressChanged(qint64 current, qint64 total) +{ + setProgress(current / 2, total); +} + +void InstanceImportTask::processZipPack() +{ + setStatus(tr("Extracting modpack")); + QDir extractDir(m_stagingPath); + qDebug() << "Attempting to create instance from" << m_archivePath; + + // open the zip and find relevant files in it + m_packZip.reset(new QuaZip(m_archivePath)); + if (!m_packZip->open(QuaZip::mdUnzip)) + { + emitFailed(tr("Unable to open supplied modpack zip file.")); + return; + } + + QStringList blacklist = {"instance.cfg", "manifest.json"}; + QString mmcFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "instance.cfg"); + bool technicFound = QuaZipDir(m_packZip.get()).exists("/bin/modpack.jar") || QuaZipDir(m_packZip.get()).exists("/bin/version.json"); + QString modrinthFound = MMCZip::findFolderOfFileInZip(m_packZip.get(), "modrinth.index.json"); + QString root; + if(!mmcFound.isNull()) + { + // process as MultiMC instance/pack + qDebug() << "MultiMC:" << mmcFound; + root = mmcFound; + m_modpackType = ModpackType::MultiMC; + } + else if (technicFound) + { + // process as Technic pack + qDebug() << "Technic:" << technicFound; + extractDir.mkpath(".minecraft"); + extractDir.cd(".minecraft"); + m_modpackType = ModpackType::Technic; + } + else if(!modrinthFound.isNull()) + { + // process as Modrinth pack + qDebug() << "Modrinth:" << modrinthFound; + root = modrinthFound; + m_modpackType = ModpackType::Modrinth; + } + if(m_modpackType == ModpackType::Unknown) + { + emitFailed(tr("Archive does not contain a recognized modpack type.")); + return; + } + + // make sure we extract just the pack + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), root, extractDir.absolutePath()); + connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &InstanceImportTask::extractFinished); + connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &InstanceImportTask::extractAborted); + m_extractFutureWatcher.setFuture(m_extractFuture); +} + +void InstanceImportTask::extractFinished() +{ + m_packZip.reset(); + if (!m_extractFuture.result()) + { + emitFailed(tr("Failed to extract modpack")); + return; + } + QDir extractDir(m_stagingPath); + + qDebug() << "Fixing permissions for extracted pack files..."; + QDirIterator it(extractDir, QDirIterator::Subdirectories); + while (it.hasNext()) + { + auto filepath = it.next(); + QFileInfo file(filepath); + auto permissions = QFile::permissions(filepath); + auto origPermissions = permissions; + if(file.isDir()) + { + // Folder +rwx for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser; + } + else + { + // File +rw for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + } + if(origPermissions != permissions) + { + if(!QFile::setPermissions(filepath, permissions)) + { + logWarning(tr("Could not fix permissions for %1").arg(filepath)); + } + else + { + qDebug() << "Fixed" << filepath; + } + } + } + + switch(m_modpackType) + { + case ModpackType::MultiMC: + processMultiMC(); + return; + case ModpackType::Technic: + processTechnic(); + return; + case ModpackType::Modrinth: + processModrinth(); + return; + case ModpackType::Unknown: + emitFailed(tr("Archive does not contain a recognized modpack type.")); + return; + } +} + +void InstanceImportTask::extractAborted() +{ + emitFailed(tr("Instance import has been aborted.")); + return; +} + +void InstanceImportTask::processTechnic() +{ + shared_qobject_ptr packProcessor = new Technic::TechnicPackProcessor(); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &InstanceImportTask::emitSucceeded); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &InstanceImportTask::emitFailed); + packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath); +} + +void InstanceImportTask::processMultiMC() +{ + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(configPath); + instanceSettings->registerSetting("InstanceType", "Legacy"); + + NullInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + + // reset time played on import... because packs. + instance.resetTimePlayed(); + + // set a new nice name + instance.setName(m_instName); + + // if the icon was specified by user, use that. otherwise pull icon from the pack + if (m_instIcon != "default") + { + instance.setIconKey(m_instIcon); + } + else + { + m_instIcon = instance.iconKey(); + + auto importIconPath = IconUtils::findBestIconIn(instance.instanceRoot(), m_instIcon); + if (!importIconPath.isNull() && QFile::exists(importIconPath)) + { + // import icon + auto iconList = APPLICATION->icons(); + if (iconList->iconFileExists(m_instIcon)) + { + iconList->deleteIcon(m_instIcon); + } + iconList->installIcons({importIconPath}); + } + } + emitSucceeded(); +} + +namespace { +bool mergeOverrides(const QString &fromDir, const QString &toDir) { + QDir dir(fromDir); + if(!dir.exists()) { + return true; + } + if(!FS::ensureFolderPathExists(toDir)) { + return false; + } + const int absSourcePathLength = dir.absoluteFilePath(fromDir).length(); + + QDirIterator it(fromDir, QDirIterator::Subdirectories); + while (it.hasNext()){ + it.next(); + const auto fileInfo = it.fileInfo(); + auto fileName = fileInfo.fileName(); + if(fileName == "." || fileName == "..") { + continue; + } + const QString subPathStructure = fileInfo.absoluteFilePath().mid(absSourcePathLength); + const QString constructedAbsolutePath = toDir + subPathStructure; + + if(fileInfo.isDir()){ + //Create directory in target folder + dir.mkpath(constructedAbsolutePath); + } else if(fileInfo.isFile()) { + QFileInfo targetFileInfo(constructedAbsolutePath); + if(targetFileInfo.exists()) { + continue; + } + // move + QFile::rename(fileInfo.absoluteFilePath(), constructedAbsolutePath); + } + } + + dir.removeRecursively(); + return true; +} + +} + +void InstanceImportTask::processModrinth() { + std::vector files; + QString minecraftVersion, fabricVersion, quiltVersion, forgeVersion, neoforgeVersion; + try + { + QString indexPath = FS::PathCombine(m_stagingPath, "modrinth.index.json"); + auto doc = Json::requireDocument(indexPath); + auto obj = Json::requireObject(doc, "modrinth.index.json"); + int formatVersion = Json::requireInteger(obj, "formatVersion", "modrinth.index.json"); + if (formatVersion == 1) + { + auto game = Json::requireString(obj, "game", "modrinth.index.json"); + if (game != "minecraft") + { + throw JSONValidationError("Unknown game: " + game); + } + + auto jsonFiles = Json::requireIsArrayOf(obj, "files", "modrinth.index.json"); + for(auto & obj: jsonFiles) { + Modrinth::File file; + auto dirtyPath = Json::requireString(obj, "path"); + dirtyPath.replace('\\', '/'); + auto simplifiedPath = QDir::cleanPath(dirtyPath); + QFileInfo fileInfo (simplifiedPath); + if(simplifiedPath.startsWith("../") || simplifiedPath.contains("/../") || fileInfo.isAbsolute()) { + throw JSONValidationError("Invalid path found in modpack files:\n\n" + simplifiedPath); + } + file.path = simplifiedPath; + + // env doesn't have to be present, in that case mod is required + auto env = Json::ensureObject(obj, "env"); + auto clientEnv = Json::ensureString(env, "client", "required"); + + if(clientEnv == "required") { + // NOOP + } + else if(clientEnv == "optional") { + file.path += ".disabled"; + } + else if(clientEnv == "unsupported") { + continue; + } + + QJsonObject hashes = Json::requireObject(obj, "hashes"); + QString hash; + QCryptographicHash::Algorithm hashAlgorithm; + hash = Json::ensureString(hashes, "sha256"); + hashAlgorithm = QCryptographicHash::Sha256; + if (hash.isEmpty()) + { + hash = Json::ensureString(hashes, "sha512"); + hashAlgorithm = QCryptographicHash::Sha512; + if (hash.isEmpty()) + { + hash = Json::ensureString(hashes, "sha1"); + hashAlgorithm = QCryptographicHash::Sha1; + if (hash.isEmpty()) + { + throw JSONValidationError("No hash found for: " + file.path); + } + } + } + file.hash = QByteArray::fromHex(hash.toLatin1()); + file.hashAlgorithm = hashAlgorithm; + // Do not use requireUrl, which uses StrictMode, instead use QUrl's default TolerantMode (as Modrinth seems to incorrectly handle spaces) + file.download = Json::requireValueString(Json::ensureArray(obj, "downloads").first(), "Download URL for " + file.path); + if (!file.download.isValid()) + { + throw JSONValidationError("Download URL for " + file.path + " is not a correctly formatted URL"); + } + files.push_back(file); + } + + auto dependencies = Json::requireObject(obj, "dependencies", "modrinth.index.json"); + for (auto it = dependencies.begin(), end = dependencies.end(); it != end; ++it) + { + QString name = it.key(); + if (name == "minecraft") + { + if (!minecraftVersion.isEmpty()) + throw JSONValidationError("Duplicate Minecraft version"); + minecraftVersion = Json::requireValueString(*it, "Minecraft version"); + } + else if (name == "fabric-loader") + { + if (!fabricVersion.isEmpty()) + throw JSONValidationError("Duplicate Fabric Loader version"); + fabricVersion = Json::requireValueString(*it, "Fabric Loader version"); + } + else if (name == "quilt-loader") + { + if (!quiltVersion.isEmpty()) + throw JSONValidationError("Duplicate Quilt Loader version"); + quiltVersion = Json::requireValueString(*it, "Quilt Loader version"); + } + else if (name == "forge") + { + if (!forgeVersion.isEmpty()) + throw JSONValidationError("Duplicate Forge version"); + forgeVersion = Json::requireValueString(*it, "Forge version"); + } + else if (name == "neoforge") + { + if (!neoforgeVersion.isEmpty()) + throw JSONValidationError("Duplicate NeoForge version"); + neoforgeVersion = Json::requireValueString(*it, "NeoForge version"); + } + else + { + throw JSONValidationError("Unknown dependency type: " + name); + } + } + } + else + { + throw JSONValidationError(QStringLiteral("Unknown format version: %s").arg(formatVersion)); + } + QFile::remove(indexPath); + } + catch (const JSONValidationError &e) + { + emitFailed(tr("Could not understand pack index:\n") + e.cause()); + return; + } + QString clientOverridePath = FS::PathCombine(m_stagingPath, "client-overrides"); + if (QFile::exists(clientOverridePath)) { + QString mcPath = FS::PathCombine(m_stagingPath, ".minecraft"); + if (!QFile::rename(clientOverridePath, mcPath)) { + emitFailed(tr("Could not rename the overrides folder:\n") + "overrides"); + return; + } + } + + // TODO: only extract things we actually want instead of everything only to just delete it afterwards ... + if(!mergeOverrides(FS::PathCombine(m_stagingPath, "client-overrides"), FS::PathCombine(m_stagingPath, ".minecraft"))) { + emitFailed(tr("Could not merge the overrides folder:\n") + "client-overrides"); + return; + } + + if(!mergeOverrides(FS::PathCombine(m_stagingPath, "overrides"), FS::PathCombine(m_stagingPath, ".minecraft"))) { + emitFailed(tr("Could not merge the overrides folder:\n") + "overrides"); + return; + } + + FS::deletePath(FS::PathCombine(m_stagingPath, "server-overrides")); + + + QString configPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(configPath); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", minecraftVersion, true); + if (!fabricVersion.isEmpty()) + components->setComponentVersion("net.fabricmc.fabric-loader", fabricVersion, true); + if (!quiltVersion.isEmpty()) + components->setComponentVersion("org.quiltmc.quilt-loader", quiltVersion, true); + if (!forgeVersion.isEmpty()) + components->setComponentVersion("net.minecraftforge", forgeVersion, true); + if (!neoforgeVersion.isEmpty()) + components->setComponentVersion("net.neoforged", neoforgeVersion, true); + if (m_instIcon != "default") + { + instance.setIconKey(m_instIcon); + } + instance.setName(m_instName); + instance.saveNow(); + + m_filesNetJob = new NetJob(tr("Mod download"), APPLICATION->network()); + for (auto &file : files) + { + auto path = FS::PathCombine(m_stagingPath, ".minecraft", file.path); + qDebug() << "Will download" << file.download << "to" << path; + auto dl = Net::Download::makeFile(file.download, path); + dl->addValidator(new Net::ChecksumValidator(file.hashAlgorithm, file.hash)); + m_filesNetJob->addNetAction(dl); + } + connect(m_filesNetJob.get(), &NetJob::succeeded, this, [&]() + { + m_filesNetJob.reset(); + emitSucceeded(); + }); + connect(m_filesNetJob.get(), &NetJob::failed, [&](const QString &reason) + { + m_filesNetJob.reset(); + emitFailed(reason); + }); + connect(m_filesNetJob.get(), &NetJob::progress, [&](qint64 current, qint64 total) + { + setProgress(current, total); + }); + setStatus(tr("Downloading mods...")); + m_filesNetJob->start(); +} diff --git a/ultimmc/launcher/InstanceImportTask.h b/ultimmc/launcher/InstanceImportTask.h new file mode 100644 index 0000000..c161f10 --- /dev/null +++ b/ultimmc/launcher/InstanceImportTask.h @@ -0,0 +1,73 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "InstanceTask.h" +#include "net/NetJob.h" +#include +#include +#include +#include "settings/SettingsObject.h" +#include "QObjectPtr.h" + +#include + +class QuaZip; +namespace Flame +{ + class FileResolvingTask; +} + +class InstanceImportTask : public InstanceTask +{ + Q_OBJECT +public: + explicit InstanceImportTask(const QUrl sourceUrl); + +protected: + //! Entry point for tasks. + virtual void executeTask() override; + +private: + void processZipPack(); + void processMultiMC(); + void processTechnic(); + void processFlame(); + void processModrinth(); + +private slots: + void downloadSucceeded(); + void downloadFailed(QString reason); + void downloadProgressChanged(qint64 current, qint64 total); + void extractFinished(); + void extractAborted(); + +private: /* data */ + NetJob::Ptr m_filesNetJob; + shared_qobject_ptr m_modIdResolver; + QUrl m_sourceUrl; + QString m_archivePath; + bool m_downloadRequired = false; + std::unique_ptr m_packZip; + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; + enum class ModpackType{ + Unknown, + MultiMC, + Technic, + Modrinth, + } m_modpackType = ModpackType::Unknown; +}; diff --git a/ultimmc/launcher/InstanceList.cpp b/ultimmc/launcher/InstanceList.cpp new file mode 100644 index 0000000..ad18740 --- /dev/null +++ b/ultimmc/launcher/InstanceList.cpp @@ -0,0 +1,912 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "InstanceList.h" +#include "BaseInstance.h" +#include "InstanceTask.h" +#include "settings/INISettingsObject.h" +#include "minecraft/legacy/LegacyInstance.h" +#include "NullInstance.h" +#include "minecraft/MinecraftInstance.h" +#include "FileSystem.h" +#include "ExponentialSeries.h" +#include "WatchLock.h" + +const static int GROUP_FILE_FORMAT_VERSION = 1; + +InstanceList::InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent) + : QAbstractListModel(parent), m_globalSettings(settings) +{ + resumeWatch(); + // Create aand normalize path + if (!QDir::current().exists(instDir)) + { + QDir::current().mkpath(instDir); + } + + connect(this, &InstanceList::instancesChanged, this, &InstanceList::providerUpdated); + + // NOTE: canonicalPath requires the path to exist. Do not move this above the creation block! + m_instDir = QDir(instDir).canonicalPath(); + m_watcher = new QFileSystemWatcher(this); + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &InstanceList::instanceDirContentsChanged); + m_watcher->addPath(m_instDir); +} + +InstanceList::~InstanceList() +{ +} + +Qt::DropActions InstanceList::supportedDragActions() const +{ + return Qt::MoveAction; +} + +Qt::DropActions InstanceList::supportedDropActions() const +{ + return Qt::MoveAction; +} + +bool InstanceList::canDropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) const +{ + if(data && data->hasFormat("application/x-instanceid")) { + return true; + } + return false; +} + +bool InstanceList::dropMimeData(const QMimeData* data, Qt::DropAction action, int row, int column, const QModelIndex& parent) +{ + if(data && data->hasFormat("application/x-instanceid")) { + return true; + } + return false; +} + +QStringList InstanceList::mimeTypes() const +{ + auto types = QAbstractListModel::mimeTypes(); + types.push_back("application/x-instanceid"); + return types; +} + +QMimeData * InstanceList::mimeData(const QModelIndexList& indexes) const +{ + auto mimeData = QAbstractListModel::mimeData(indexes); + if(indexes.size() == 1) { + auto instanceId = data(indexes[0], InstanceIDRole).toString(); + mimeData->setData("application/x-instanceid", instanceId.toUtf8()); + } + return mimeData; +} + + +int InstanceList::rowCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent); + return m_instances.count(); +} + +QModelIndex InstanceList::index(int row, int column, const QModelIndex &parent) const +{ + Q_UNUSED(parent); + if (row < 0 || row >= m_instances.size()) + return QModelIndex(); + return createIndex(row, column, (void *)m_instances.at(row).get()); +} + +QVariant InstanceList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + { + return QVariant(); + } + BaseInstance *pdata = static_cast(index.internalPointer()); + switch (role) + { + case InstancePointerRole: + { + QVariant v = qVariantFromValue((void *)pdata); + return v; + } + case InstanceIDRole: + { + return pdata->id(); + } + case Qt::EditRole: + case Qt::DisplayRole: + { + return pdata->name(); + } + case Qt::AccessibleTextRole: + { + return tr("%1 Instance").arg(pdata->name()); + } + case Qt::ToolTipRole: + { + return pdata->instanceRoot(); + } + case Qt::DecorationRole: + { + return pdata->iconKey(); + } + // HACK: see InstanceView.h in gui! + case GroupRole: + { + return getInstanceGroup(pdata->id()); + } + default: + break; + } + return QVariant(); +} + +bool InstanceList::setData(const QModelIndex& index, const QVariant& value, int role) +{ + if (!index.isValid()) + { + return false; + } + if(role != Qt::EditRole) + { + return false; + } + BaseInstance *pdata = static_cast(index.internalPointer()); + auto newName = value.toString(); + if(pdata->name() == newName) + { + return true; + } + pdata->setName(newName); + return true; +} + +Qt::ItemFlags InstanceList::flags(const QModelIndex &index) const +{ + Qt::ItemFlags f; + if (index.isValid()) + { + f |= (Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEditable); + } + return f; +} + +GroupId InstanceList::getInstanceGroup(const InstanceId& id) const +{ + auto inst = getInstanceById(id); + if(!inst) + { + return GroupId(); + } + auto iter = m_instanceGroupIndex.find(inst->id()); + if(iter != m_instanceGroupIndex.end()) + { + return *iter; + } + return GroupId(); +} + +void InstanceList::setInstanceGroup(const InstanceId& id, const GroupId& name) +{ + auto inst = getInstanceById(id); + if(!inst) + { + qDebug() << "Attempt to set a null instance's group"; + return; + } + + bool changed = false; + auto iter = m_instanceGroupIndex.find(inst->id()); + if(iter != m_instanceGroupIndex.end()) + { + if(*iter != name) + { + *iter = name; + changed = true; + } + } + else + { + changed = true; + m_instanceGroupIndex[id] = name; + } + + if(changed) + { + m_groupNameCache.insert(name); + auto idx = getInstIndex(inst.get()); + emit dataChanged(index(idx), index(idx), {GroupRole}); + saveGroupList(); + } +} + +QStringList InstanceList::getGroups() +{ + return m_groupNameCache.toList(); +} + +void InstanceList::deleteGroup(const QString& name) +{ + bool removed = false; + qDebug() << "Delete group" << name; + for(auto & instance: m_instances) + { + const auto & instID = instance->id(); + auto instGroupName = getInstanceGroup(instID); + if(instGroupName == name) + { + m_instanceGroupIndex.remove(instID); + qDebug() << "Remove" << instID << "from group" << name; + removed = true; + auto idx = getInstIndex(instance.get()); + if(idx > 0) + { + emit dataChanged(index(idx), index(idx), {GroupRole}); + } + } + } + if(removed) + { + saveGroupList(); + } +} + +bool InstanceList::isGroupCollapsed(const QString& group) +{ + return m_collapsedGroups.contains(group); +} + +void InstanceList::deleteInstance(const InstanceId& id) +{ + auto inst = getInstanceById(id); + if(!inst) + { + qDebug() << "Cannot delete instance" << id << ". No such instance is present (deleted externally?)."; + return; + } + + if(m_instanceGroupIndex.remove(id)) + { + saveGroupList(); + } + + qDebug() << "Will delete instance" << id; + if(!FS::deletePath(inst->instanceRoot())) + { + qWarning() << "Deletion of instance" << id << "has not been completely successful ..."; + return; + } + + qDebug() << "Instance" << id << "has been deleted by the launcher."; +} + +static QMap getIdMapping(const QList &list) +{ + QMap out; + int i = 0; + for(auto & item: list) + { + auto id = item->id(); + if(out.contains(id)) + { + qWarning() << "Duplicate ID" << id << "in instance list"; + } + out[id] = std::make_pair(item, i); + i++; + } + return out; +} + +QList< InstanceId > InstanceList::discoverInstances() +{ + qDebug() << "Discovering instances in" << m_instDir; + QList out; + QDirIterator iter(m_instDir, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); + while (iter.hasNext()) + { + QString subDir = iter.next(); + QFileInfo dirInfo(subDir); + if (!QFileInfo(FS::PathCombine(subDir, "instance.cfg")).exists()) + continue; + // if it is a symlink, ignore it if it goes to the instance folder + if(dirInfo.isSymLink()) + { + QFileInfo targetInfo(dirInfo.symLinkTarget()); + QFileInfo instDirInfo(m_instDir); + if(targetInfo.canonicalPath() == instDirInfo.canonicalFilePath()) + { + qDebug() << "Ignoring symlink" << subDir << "that leads into the instances folder"; + continue; + } + } + auto id = dirInfo.fileName(); + out.append(id); + qDebug() << "Found instance ID" << id; + } + instanceSet = out.toSet(); + m_instancesProbed = true; + return out; +} + +InstanceList::InstListError InstanceList::loadList() +{ + auto existingIds = getIdMapping(m_instances); + + QList newList; + + for(auto & id: discoverInstances()) + { + if(existingIds.contains(id)) + { + auto instPair = existingIds[id]; + existingIds.remove(id); + qDebug() << "Should keep and soft-reload" << id; + } + else + { + InstancePtr instPtr = loadInstance(id); + if(instPtr) + { + newList.append(instPtr); + } + } + } + + // TODO: looks like a general algorithm with a few specifics inserted. Do something about it. + if(!existingIds.isEmpty()) + { + // get the list of removed instances and sort it by their original index, from last to first + auto deadList = existingIds.values(); + auto orderSortPredicate = [](const InstanceLocator & a, const InstanceLocator & b) -> bool + { + return a.second > b.second; + }; + std::sort(deadList.begin(), deadList.end(), orderSortPredicate); + // remove the contiguous ranges of rows + int front_bookmark = -1; + int back_bookmark = -1; + int currentItem = -1; + auto removeNow = [&]() + { + beginRemoveRows(QModelIndex(), front_bookmark, back_bookmark); + m_instances.erase(m_instances.begin() + front_bookmark, m_instances.begin() + back_bookmark + 1); + endRemoveRows(); + front_bookmark = -1; + back_bookmark = currentItem; + }; + for(auto & removedItem: deadList) + { + auto instPtr = removedItem.first; + instPtr->invalidate(); + currentItem = removedItem.second; + if(back_bookmark == -1) + { + // no bookmark yet + back_bookmark = currentItem; + } + else if(currentItem == front_bookmark - 1) + { + // part of contiguous sequence, continue + } + else + { + // seam between previous and current item + removeNow(); + } + front_bookmark = currentItem; + } + if(back_bookmark != -1) + { + removeNow(); + } + } + if(newList.size()) + { + add(newList); + } + m_dirty = false; + updateTotalPlayTime(); + return NoError; +} + +void InstanceList::updateTotalPlayTime() +{ + totalPlayTime = 0; + for(auto const& itr : m_instances) + { + totalPlayTime += itr.get()->totalTimePlayed(); + } +} + +void InstanceList::saveNow() +{ + for(auto & item: m_instances) + { + item->saveNow(); + } +} + +void InstanceList::add(const QList &t) +{ + beginInsertRows(QModelIndex(), m_instances.count(), m_instances.count() + t.size() - 1); + m_instances.append(t); + for(auto & ptr : t) + { + connect(ptr.get(), &BaseInstance::propertiesChanged, this, &InstanceList::propertiesChanged); + } + endInsertRows(); +} + +void InstanceList::resumeWatch() +{ + if(m_watchLevel > 0) + { + qWarning() << "Bad suspend level resume in instance list"; + return; + } + m_watchLevel++; + if(m_watchLevel > 0 && m_dirty) + { + loadList(); + } +} + +void InstanceList::suspendWatch() +{ + m_watchLevel --; +} + +void InstanceList::providerUpdated() +{ + m_dirty = true; + if(m_watchLevel == 1) + { + loadList(); + } +} + +InstancePtr InstanceList::getInstanceById(QString instId) const +{ + if(instId.isEmpty()) + return InstancePtr(); + for(auto & inst: m_instances) + { + if (inst->id() == instId) + { + return inst; + } + } + return InstancePtr(); +} + +QModelIndex InstanceList::getInstanceIndexById(const QString &id) const +{ + return index(getInstIndex(getInstanceById(id).get())); +} + +int InstanceList::getInstIndex(BaseInstance *inst) const +{ + int count = m_instances.count(); + for (int i = 0; i < count; i++) + { + if (inst == m_instances[i].get()) + { + return i; + } + } + return -1; +} + +void InstanceList::propertiesChanged(BaseInstance *inst) +{ + int i = getInstIndex(inst); + if (i != -1) + { + emit dataChanged(index(i), index(i)); + updateTotalPlayTime(); + } +} + +InstancePtr InstanceList::loadInstance(const InstanceId& id) +{ + if(!m_groupsLoaded) + { + loadGroupList(); + } + + auto instanceRoot = FS::PathCombine(m_instDir, id); + auto instanceSettings = std::make_shared(FS::PathCombine(instanceRoot, "instance.cfg")); + InstancePtr inst; + + instanceSettings->registerSetting("InstanceType", "Legacy"); + + QString inst_type = instanceSettings->get("InstanceType").toString(); + + if (inst_type == "OneSix" || inst_type == "Nostalgia") + { + inst.reset(new MinecraftInstance(m_globalSettings, instanceSettings, instanceRoot)); + } + else if (inst_type == "Legacy") + { + inst.reset(new LegacyInstance(m_globalSettings, instanceSettings, instanceRoot)); + } + else + { + inst.reset(new NullInstance(m_globalSettings, instanceSettings, instanceRoot)); + } + qDebug() << "Loaded instance " << inst->name() << " from " << inst->instanceRoot(); + return inst; +} + +void InstanceList::saveGroupList() +{ + qDebug() << "Will save group list now."; + if(!m_instancesProbed) + { + qDebug() << "Group saving prevented because we don't know the full list of instances yet."; + return; + } + WatchLock foo(m_watcher, m_instDir); + QString groupFileName = m_instDir + "/instgroups.json"; + QMap> reverseGroupMap; + for (auto iter = m_instanceGroupIndex.begin(); iter != m_instanceGroupIndex.end(); iter++) + { + QString id = iter.key(); + QString group = iter.value(); + if (group.isEmpty()) + continue; + if(!instanceSet.contains(id)) + { + qDebug() << "Skipping saving missing instance" << id << "to groups list."; + continue; + } + + if (!reverseGroupMap.count(group)) + { + QSet set; + set.insert(id); + reverseGroupMap[group] = set; + } + else + { + QSet &set = reverseGroupMap[group]; + set.insert(id); + } + } + QJsonObject toplevel; + toplevel.insert("formatVersion", QJsonValue(QString("1"))); + QJsonObject groupsArr; + for (auto iter = reverseGroupMap.begin(); iter != reverseGroupMap.end(); iter++) + { + auto list = iter.value(); + auto name = iter.key(); + QJsonObject groupObj; + QJsonArray instanceArr; + groupObj.insert("hidden", QJsonValue(m_collapsedGroups.contains(name))); + for (auto item : list) + { + instanceArr.append(QJsonValue(item)); + } + groupObj.insert("instances", instanceArr); + groupsArr.insert(name, groupObj); + } + toplevel.insert("groups", groupsArr); + QJsonDocument doc(toplevel); + try + { + FS::write(groupFileName, doc.toJson()); + qDebug() << "Group list saved."; + } + catch (const FS::FileSystemException &e) + { + qCritical() << "Failed to write instance group file :" << e.cause(); + } +} + +void InstanceList::loadGroupList() +{ + qDebug() << "Will load group list now."; + + QString groupFileName = m_instDir + "/instgroups.json"; + + // if there's no group file, fail + if (!QFileInfo(groupFileName).exists()) + return; + + QByteArray jsonData; + try + { + jsonData = FS::read(groupFileName); + } + catch (const FS::FileSystemException &e) + { + qCritical() << "Failed to read instance group file :" << e.cause(); + return; + } + + QJsonParseError error; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &error); + + // if the json was bad, fail + if (error.error != QJsonParseError::NoError) + { + qCritical() << QString("Failed to parse instance group file: %1 at offset %2") + .arg(error.errorString(), QString::number(error.offset)) + .toUtf8(); + return; + } + + // if the root of the json wasn't an object, fail + if (!jsonDoc.isObject()) + { + qWarning() << "Invalid group file. Root entry should be an object."; + return; + } + + QJsonObject rootObj = jsonDoc.object(); + + // Make sure the format version matches, otherwise fail. + if (rootObj.value("formatVersion").toVariant().toInt() != GROUP_FILE_FORMAT_VERSION) + return; + + // Get the groups. if it's not an object, fail + if (!rootObj.value("groups").isObject()) + { + qWarning() << "Invalid group list JSON: 'groups' should be an object."; + return; + } + + QSet groupSet; + m_instanceGroupIndex.clear(); + + // Iterate through all the groups. + QJsonObject groupMapping = rootObj.value("groups").toObject(); + for (QJsonObject::iterator iter = groupMapping.begin(); iter != groupMapping.end(); iter++) + { + QString groupName = iter.key(); + + // If not an object, complain and skip to the next one. + if (!iter.value().isObject()) + { + qWarning() << QString("Group '%1' in the group list should be an object.").arg(groupName).toUtf8(); + continue; + } + + QJsonObject groupObj = iter.value().toObject(); + if (!groupObj.value("instances").isArray()) + { + qWarning() << QString("Group '%1' in the group list is invalid. It should contain an array called 'instances'.").arg(groupName).toUtf8(); + continue; + } + + // keep a list/set of groups for choosing + groupSet.insert(groupName); + + auto hidden = groupObj.value("hidden").toBool(false); + if(hidden) { + m_collapsedGroups.insert(groupName); + } + + // Iterate through the list of instances in the group. + QJsonArray instancesArray = groupObj.value("instances").toArray(); + + for (QJsonArray::iterator iter2 = instancesArray.begin(); iter2 != instancesArray.end(); iter2++) + { + m_instanceGroupIndex[(*iter2).toString()] = groupName; + } + } + m_groupsLoaded = true; + m_groupNameCache.unite(groupSet); + qDebug() << "Group list loaded."; +} + +void InstanceList::instanceDirContentsChanged(const QString& path) +{ + Q_UNUSED(path); + emit instancesChanged(); +} + +void InstanceList::on_InstFolderChanged(const Setting &setting, QVariant value) +{ + QString newInstDir = QDir(value.toString()).canonicalPath(); + if(newInstDir != m_instDir) + { + if(m_groupsLoaded) + { + saveGroupList(); + } + m_instDir = newInstDir; + m_groupsLoaded = false; + emit instancesChanged(); + } +} + +void InstanceList::on_GroupStateChanged(const QString& group, bool collapsed) +{ + qDebug() << "Group" << group << (collapsed ? "collapsed" : "expanded"); + if(collapsed) { + m_collapsedGroups.insert(group); + } else { + m_collapsedGroups.remove(group); + } + saveGroupList(); +} + +class InstanceStaging : public Task +{ +Q_OBJECT + const unsigned minBackoff = 1; + const unsigned maxBackoff = 16; +public: + InstanceStaging ( + InstanceList * parent, + Task * child, + const QString & stagingPath, + const QString& instanceName, + const QString& groupName ) + : backoff(minBackoff, maxBackoff) + { + m_parent = parent; + m_child.reset(child); + connect(child, &Task::succeeded, this, &InstanceStaging::childSucceded); + connect(child, &Task::failed, this, &InstanceStaging::childFailed); + connect(child, &Task::status, this, &InstanceStaging::setStatus); + connect(child, &Task::progress, this, &InstanceStaging::setProgress); + m_instanceName = instanceName; + m_groupName = groupName; + m_stagingPath = stagingPath; + m_backoffTimer.setSingleShot(true); + connect(&m_backoffTimer, &QTimer::timeout, this, &InstanceStaging::childSucceded); + } + + virtual ~InstanceStaging() {}; + + + // FIXME/TODO: add ability to abort during instance commit retries + bool abort() override + { + if(m_child && m_child->canAbort()) + { + return m_child->abort(); + } + return false; + } + bool canAbort() const override + { + if(m_child && m_child->canAbort()) + { + return true; + } + return false; + } + +protected: + virtual void executeTask() override + { + m_child->start(); + } + QStringList warnings() const override + { + return m_child->warnings(); + } + +private slots: + void childSucceded() + { + unsigned sleepTime = backoff(); + if(m_parent->commitStagedInstance(m_stagingPath, m_instanceName, m_groupName)) + { + emitSucceeded(); + return; + } + // we actually failed, retry? + if(sleepTime == maxBackoff) + { + emitFailed(tr("Failed to commit instance, even after multiple retries. It is being blocked by something.")); + return; + } + qDebug() << "Failed to commit instance" << m_instanceName << "Initiating backoff:" << sleepTime; + m_backoffTimer.start(sleepTime * 500); + } + void childFailed(const QString & reason) + { + m_parent->destroyStagingPath(m_stagingPath); + emitFailed(reason); + } + +private: + /* + * WHY: the whole reason why this uses an exponential backoff retry scheme is antivirus on Windows. + * Basically, it starts messing things up while the launcher is extracting/creating instances + * and causes that horrible failure that is NTFS to lock files in place because they are open. + */ + ExponentialSeries backoff; + QString m_stagingPath; + InstanceList * m_parent; + unique_qobject_ptr m_child; + QString m_instanceName; + QString m_groupName; + QTimer m_backoffTimer; +}; + +Task * InstanceList::wrapInstanceTask(InstanceTask * task) +{ + auto stagingPath = getStagedInstancePath(); + task->setStagingPath(stagingPath); + task->setParentSettings(m_globalSettings); + return new InstanceStaging(this, task, stagingPath, task->name(), task->group()); +} + +QString InstanceList::getStagedInstancePath() +{ + QString key = QUuid::createUuid().toString(); + QString relPath = FS::PathCombine("_LAUNCHER_TEMP/" , key); + QDir rootPath(m_instDir); + auto path = FS::PathCombine(m_instDir, relPath); + if(!rootPath.mkpath(relPath)) + { + return QString(); + } + return path; +} + +bool InstanceList::commitStagedInstance(const QString& path, const QString& instanceName, const QString& groupName) +{ + QDir dir; + QString instID = FS::DirNameFromString(instanceName, m_instDir); + { + WatchLock lock(m_watcher, m_instDir); + QString destination = FS::PathCombine(m_instDir, instID); + if(!dir.rename(path, destination)) + { + qWarning() << "Failed to move" << path << "to" << destination; + return false; + } + m_instanceGroupIndex[instID] = groupName; + instanceSet.insert(instID); + m_groupNameCache.insert(groupName); + emit instancesChanged(); + emit instanceSelectRequest(instID); + } + saveGroupList(); + return true; +} + +bool InstanceList::destroyStagingPath(const QString& keyPath) +{ + return FS::deletePath(keyPath); +} + +int InstanceList::getTotalPlayTime() { + updateTotalPlayTime(); + return totalPlayTime; +} + +#include "InstanceList.moc" diff --git a/ultimmc/launcher/InstanceList.h b/ultimmc/launcher/InstanceList.h new file mode 100644 index 0000000..bc6c3af --- /dev/null +++ b/ultimmc/launcher/InstanceList.h @@ -0,0 +1,183 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include "BaseInstance.h" + +#include "QObjectPtr.h" + +class QFileSystemWatcher; +class InstanceTask; +using InstanceId = QString; +using GroupId = QString; +using InstanceLocator = std::pair; + +enum class InstCreateError +{ + NoCreateError = 0, + NoSuchVersion, + UnknownCreateError, + InstExists, + CantCreateDir +}; + +enum class GroupsState +{ + NotLoaded, + Steady, + Dirty +}; + + +class InstanceList : public QAbstractListModel +{ + Q_OBJECT + +public: + explicit InstanceList(SettingsObjectPtr settings, const QString & instDir, QObject *parent = 0); + virtual ~InstanceList(); + +public: + QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + bool setData(const QModelIndex & index, const QVariant & value, int role) override; + + enum AdditionalRoles + { + GroupRole = Qt::UserRole, + InstancePointerRole = 0x34B1CB48, ///< Return pointer to real instance + InstanceIDRole = 0x34B1CB49 ///< Return id if the instance + }; + /*! + * \brief Error codes returned by functions in the InstanceList class. + * NoError Indicates that no error occurred. + * UnknownError indicates that an unspecified error occurred. + */ + enum InstListError + { + NoError = 0, + UnknownError + }; + + InstancePtr at(int i) const + { + return m_instances.at(i); + } + + int count() const + { + return m_instances.count(); + } + + InstListError loadList(); + void saveNow(); + + InstancePtr getInstanceById(QString id) const; + QModelIndex getInstanceIndexById(const QString &id) const; + QStringList getGroups(); + bool isGroupCollapsed(const QString &groupName); + + GroupId getInstanceGroup(const InstanceId & id) const; + void setInstanceGroup(const InstanceId & id, const GroupId& name); + + void deleteGroup(const GroupId & name); + void deleteInstance(const InstanceId & id); + + // Wrap an instance creation task in some more task machinery and make it ready to be used + Task * wrapInstanceTask(InstanceTask * task); + + /** + * Create a new empty staging area for instance creation and @return a path/key top commit it later. + * Used by instance manipulation tasks. + */ + QString getStagedInstancePath(); + + /** + * Commit the staging area given by @keyPath to the provider - used when creation succeeds. + * Used by instance manipulation tasks. + */ + bool commitStagedInstance(const QString & keyPath, const QString& instanceName, const QString & groupName); + + /** + * Destroy a previously created staging area given by @keyPath - used when creation fails. + * Used by instance manipulation tasks. + */ + bool destroyStagingPath(const QString & keyPath); + + int getTotalPlayTime(); + + Qt::DropActions supportedDragActions() const override; + + Qt::DropActions supportedDropActions() const override; + + bool canDropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) const override; + + bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override; + + QStringList mimeTypes() const override; + QMimeData *mimeData(const QModelIndexList &indexes) const override; + +signals: + void dataIsInvalid(); + void instancesChanged(); + void instanceSelectRequest(QString instanceId); + void groupsChanged(QSet groups); + +public slots: + void on_InstFolderChanged(const Setting &setting, QVariant value); + void on_GroupStateChanged(const QString &group, bool collapsed); + +private slots: + void propertiesChanged(BaseInstance *inst); + void providerUpdated(); + void instanceDirContentsChanged(const QString &path); + +private: + int getInstIndex(BaseInstance *inst) const; + void updateTotalPlayTime(); + void suspendWatch(); + void resumeWatch(); + void add(const QList &list); + void loadGroupList(); + void saveGroupList(); + QList discoverInstances(); + InstancePtr loadInstance(const InstanceId& id); + +private: + int m_watchLevel = 0; + int totalPlayTime = 0; + bool m_dirty = false; + QList m_instances; + QSet m_groupNameCache; + + SettingsObjectPtr m_globalSettings; + QString m_instDir; + QFileSystemWatcher * m_watcher; + // FIXME: this is so inefficient that looking at it is almost painful. + QSet m_collapsedGroups; + QMap m_instanceGroupIndex; + QSet instanceSet; + bool m_groupsLoaded = false; + bool m_instancesProbed = false; +}; diff --git a/ultimmc/launcher/InstancePageProvider.h b/ultimmc/launcher/InstancePageProvider.h new file mode 100644 index 0000000..76d183f --- /dev/null +++ b/ultimmc/launcher/InstancePageProvider.h @@ -0,0 +1,76 @@ +#pragma once +#include "minecraft/MinecraftInstance.h" +#include "minecraft/legacy/LegacyInstance.h" +#include +#include "ui/pages/BasePage.h" +#include "ui/pages/BasePageProvider.h" +#include "ui/pages/instance/LogPage.h" +#include "ui/pages/instance/VersionPage.h" +#include "ui/pages/instance/ModFolderPage.h" +#include "ui/pages/instance/ResourcePackPage.h" +#include "ui/pages/instance/TexturePackPage.h" +#include "ui/pages/instance/ShaderPackPage.h" +#include "ui/pages/instance/NotesPage.h" +#include "ui/pages/instance/ScreenshotsPage.h" +#include "ui/pages/instance/InstanceSettingsPage.h" +#include "ui/pages/instance/OtherLogsPage.h" +#include "ui/pages/instance/LegacyUpgradePage.h" +#include "ui/pages/instance/WorldListPage.h" +#include "ui/pages/instance/ServersPage.h" +#include "ui/pages/instance/GameOptionsPage.h" + +class InstancePageProvider : public QObject, public BasePageProvider +{ + Q_OBJECT +public: + explicit InstancePageProvider(InstancePtr parent) + { + inst = parent; + } + + virtual ~InstancePageProvider() {}; + virtual QList getPages() override + { + QList values; + values.append(new LogPage(inst)); + std::shared_ptr onesix = std::dynamic_pointer_cast(inst); + if(onesix) + { + values.append(new VersionPage(onesix.get())); + auto modsPage = new ModFolderPage(onesix.get(), onesix->loaderModList(), "mods", "loadermods", tr("Loader mods"), "Loader-mods"); + modsPage->setFilter("%1 (*.zip *.jar *.litemod)"); + values.append(modsPage); + values.append(new CoreModFolderPage(onesix.get(), onesix->coreModList(), "coremods", "coremods", tr("Core mods"), "Core-mods")); + values.append(new ResourcePackPage(onesix.get())); + values.append(new TexturePackPage(onesix.get())); + values.append(new ShaderPackPage(onesix.get())); + values.append(new NotesPage(onesix.get())); + values.append(new WorldListPage(onesix, onesix->worldList())); + values.append(new ServersPage(onesix)); + // values.append(new GameOptionsPage(onesix.get())); + values.append(new ScreenshotsPage(FS::PathCombine(onesix->gameRoot(), "screenshots"))); + values.append(new InstanceSettingsPage(onesix.get())); + } + std::shared_ptr legacy = std::dynamic_pointer_cast(inst); + if(legacy) + { + values.append(new LegacyUpgradePage(legacy)); + values.append(new NotesPage(legacy.get())); + values.append(new WorldListPage(legacy, legacy->worldList())); + values.append(new ScreenshotsPage(FS::PathCombine(legacy->gameRoot(), "screenshots"))); + } + auto logMatcher = inst->getLogFileMatcher(); + if(logMatcher) + { + values.append(new OtherLogsPage(inst->getLogFileRoot(), logMatcher)); + } + return values; + } + + virtual QString dialogTitle() override + { + return tr("Edit Instance (%1)").arg(inst->name()); + } +protected: + InstancePtr inst; +}; diff --git a/ultimmc/launcher/InstanceTask.cpp b/ultimmc/launcher/InstanceTask.cpp new file mode 100644 index 0000000..dd13287 --- /dev/null +++ b/ultimmc/launcher/InstanceTask.cpp @@ -0,0 +1,9 @@ +#include "InstanceTask.h" + +InstanceTask::InstanceTask() +{ +} + +InstanceTask::~InstanceTask() +{ +} diff --git a/ultimmc/launcher/InstanceTask.h b/ultimmc/launcher/InstanceTask.h new file mode 100644 index 0000000..82e23f1 --- /dev/null +++ b/ultimmc/launcher/InstanceTask.h @@ -0,0 +1,52 @@ +#pragma once + +#include "tasks/Task.h" +#include "settings/SettingsObject.h" + +class InstanceTask : public Task +{ + Q_OBJECT +public: + explicit InstanceTask(); + virtual ~InstanceTask(); + + void setParentSettings(SettingsObjectPtr settings) + { + m_globalSettings = settings; + } + + void setStagingPath(const QString &stagingPath) + { + m_stagingPath = stagingPath; + } + + void setName(const QString &name) + { + m_instName = name; + } + QString name() const + { + return m_instName; + } + + void setIcon(const QString &icon) + { + m_instIcon = icon; + } + + void setGroup(const QString &group) + { + m_instGroup = group; + } + QString group() const + { + return m_instGroup; + } + +protected: /* data */ + SettingsObjectPtr m_globalSettings; + QString m_instName; + QString m_instIcon; + QString m_instGroup; + QString m_stagingPath; +}; diff --git a/ultimmc/launcher/JavaCommon.cpp b/ultimmc/launcher/JavaCommon.cpp new file mode 100644 index 0000000..9403591 --- /dev/null +++ b/ultimmc/launcher/JavaCommon.cpp @@ -0,0 +1,115 @@ +#include "JavaCommon.h" +#include "ui/dialogs/CustomMessageBox.h" +#include + +bool JavaCommon::checkJVMArgs(QString jvmargs, QWidget *parent) +{ + if (jvmargs.contains("-XX:PermSize=") || jvmargs.contains(QRegExp("-Xm[sx]")) + || jvmargs.contains("-XX-MaxHeapSize") || jvmargs.contains("-XX:InitialHeapSize")) + { + auto warnStr = QObject::tr( + "You tried to manually set a JVM memory option (using \"-XX:PermSize\", \"-XX-MaxHeapSize\", \"-XX:InitialHeapSize\", \"-Xmx\" or \"-Xms\").\n" + "There are dedicated boxes for these in the settings (Java tab, in the Memory group at the top).\n" + "This message will be displayed until you remove them from the JVM arguments."); + CustomMessageBox::selectable( + parent, QObject::tr("JVM arguments warning"), + warnStr, + QMessageBox::Warning)->exec(); + return false; + } + // block lunacy with passing required version to the JVM + if (jvmargs.contains(QRegExp("-version:.*"))) { + auto warnStr = QObject::tr( + "You tried to pass required java version argument to the JVM (using \"-version=xxx\"). This is not safe and will not be allowed.\n" + "This message will be displayed until you remove this from the JVM arguments."); + CustomMessageBox::selectable( + parent, QObject::tr("JVM arguments warning"), + warnStr, + QMessageBox::Warning)->exec(); + return false; + } + return true; +} + +void JavaCommon::javaWasOk(QWidget *parent, JavaCheckResult result) +{ + QString text; + text += QObject::tr("Java test succeeded!
Platform reported: %1
Java version " + "reported: %2
Java vendor " + "reported: %3
").arg(result.realPlatform, result.javaVersion.toString(), result.javaVendor); + if (result.errorLog.size()) + { + auto htmlError = result.errorLog; + htmlError.replace('\n', "
"); + text += QObject::tr("
Warnings:
%1").arg(htmlError); + } + CustomMessageBox::selectable(parent, QObject::tr("Java test success"), text, QMessageBox::Information)->show(); +} + +void JavaCommon::javaArgsWereBad(QWidget *parent, JavaCheckResult result) +{ + auto htmlError = result.errorLog; + QString text; + htmlError.replace('\n', "
"); + text += QObject::tr("The specified java binary didn't work with the arguments you provided:
"); + text += QString("%1").arg(htmlError); + CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); +} + +void JavaCommon::javaBinaryWasBad(QWidget *parent, JavaCheckResult result) +{ + QString text; + text += QObject::tr( + "The specified java binary didn't work.
You should use the auto-detect feature, " + "or set the path to the java executable.
"); + CustomMessageBox::selectable(parent, QObject::tr("Java test failure"), text, QMessageBox::Warning)->show(); +} + +void JavaCommon::TestCheck::run() +{ + if (!JavaCommon::checkJVMArgs(m_args, m_parent)) + { + emit finished(); + return; + } + checker.reset(new JavaChecker()); + connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this, + SLOT(checkFinished(JavaCheckResult))); + checker->m_path = m_path; + checker->performCheck(); +} + +void JavaCommon::TestCheck::checkFinished(JavaCheckResult result) +{ + if (result.validity != JavaCheckResult::Validity::Valid) + { + javaBinaryWasBad(m_parent, result); + emit finished(); + return; + } + checker.reset(new JavaChecker()); + connect(checker.get(), SIGNAL(checkFinished(JavaCheckResult)), this, + SLOT(checkFinishedWithArgs(JavaCheckResult))); + checker->m_path = m_path; + checker->m_args = m_args; + checker->m_minMem = m_minMem; + checker->m_maxMem = m_maxMem; + if (result.javaVersion.requiresPermGen()) + { + checker->m_permGen = m_permGen; + } + checker->performCheck(); +} + +void JavaCommon::TestCheck::checkFinishedWithArgs(JavaCheckResult result) +{ + if (result.validity == JavaCheckResult::Validity::Valid) + { + javaWasOk(m_parent, result); + emit finished(); + return; + } + javaArgsWereBad(m_parent, result); + emit finished(); +} + diff --git a/ultimmc/launcher/JavaCommon.h b/ultimmc/launcher/JavaCommon.h new file mode 100644 index 0000000..ca98145 --- /dev/null +++ b/ultimmc/launcher/JavaCommon.h @@ -0,0 +1,48 @@ +#pragma once +#include + +class QWidget; + +/** + * Common UI bits for the java pages to use. + */ +namespace JavaCommon +{ + bool checkJVMArgs(QString args, QWidget *parent); + + // Show a dialog saying that the Java binary was not usable + void javaBinaryWasBad(QWidget *parent, JavaCheckResult result); + // Show a dialog saying that the Java binary was not usable because of bad options + void javaArgsWereBad(QWidget *parent, JavaCheckResult result); + // Show a dialog saying that the Java binary was usable + void javaWasOk(QWidget *parent, JavaCheckResult result); + + class TestCheck : public QObject + { + Q_OBJECT + public: + TestCheck(QWidget *parent, QString path, QString args, int minMem, int maxMem, int permGen) + :m_parent(parent), m_path(path), m_args(args), m_minMem(minMem), m_maxMem(maxMem), m_permGen(permGen) + { + } + virtual ~TestCheck() {}; + + void run(); + + signals: + void finished(); + + private slots: + void checkFinished(JavaCheckResult result); + void checkFinishedWithArgs(JavaCheckResult result); + + private: + std::shared_ptr checker; + QWidget *m_parent = nullptr; + QString m_path; + QString m_args; + int m_minMem = 0; + int m_maxMem = 0; + int m_permGen = 64; + }; +} diff --git a/ultimmc/launcher/Json.cpp b/ultimmc/launcher/Json.cpp new file mode 100644 index 0000000..8f6908d --- /dev/null +++ b/ultimmc/launcher/Json.cpp @@ -0,0 +1,280 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#include "Json.h" + +#include + +#include "FileSystem.h" +#include + +namespace Json +{ +void write(const QJsonDocument &doc, const QString &filename) +{ + FS::write(filename, doc.toJson()); +} +void write(const QJsonObject &object, const QString &filename) +{ + write(QJsonDocument(object), filename); +} +void write(const QJsonArray &array, const QString &filename) +{ + write(QJsonDocument(array), filename); +} + +QByteArray toBinary(const QJsonObject &obj) +{ + return QJsonDocument(obj).toBinaryData(); +} +QByteArray toBinary(const QJsonArray &array) +{ + return QJsonDocument(array).toBinaryData(); +} +QByteArray toText(const QJsonObject &obj) +{ + return QJsonDocument(obj).toJson(QJsonDocument::Compact); +} +QByteArray toText(const QJsonArray &array) +{ + return QJsonDocument(array).toJson(QJsonDocument::Compact); +} + +static bool isBinaryJson(const QByteArray &data) +{ + decltype(QJsonDocument::BinaryFormatTag) tag = QJsonDocument::BinaryFormatTag; + return memcmp(data.constData(), &tag, sizeof(QJsonDocument::BinaryFormatTag)) == 0; +} +QJsonDocument requireDocument(const QByteArray &data, const QString &what) +{ + if (isBinaryJson(data)) + { + QJsonDocument doc = QJsonDocument::fromBinaryData(data); + if (doc.isNull()) + { + throw JsonException(what + ": Invalid JSON (binary JSON detected)"); + } + return doc; + } + else + { + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) + { + throw JsonException(what + ": Error parsing JSON: " + error.errorString()); + } + return doc; + } +} +QJsonDocument requireDocument(const QString &filename, const QString &what) +{ + return requireDocument(FS::read(filename), what); +} +QJsonObject requireObject(const QJsonDocument &doc, const QString &what) +{ + if (!doc.isObject()) + { + throw JsonException(what + " is not an object"); + } + return doc.object(); +} +QJsonObject requireObject(const QJsonValueRef &node, const QString &what) +{ + if (!node.isObject()) + { + throw JsonException(what + " is not an object"); + } + return node.toObject(); +} +QJsonArray requireArray(const QJsonDocument &doc, const QString &what) +{ + if (!doc.isArray()) + { + throw JsonException(what + " is not an array"); + } + return doc.array(); +} + +void writeString(QJsonObject &to, const QString &key, const QString &value) +{ + if (!value.isEmpty()) + { + to.insert(key, value); + } +} + +void writeStringList(QJsonObject &to, const QString &key, const QStringList &values) +{ + if (!values.isEmpty()) + { + QJsonArray array; + for(auto value: values) + { + array.append(value); + } + to.insert(key, array); + } +} + +template<> +QJsonValue toJson(const QUrl &url) +{ + return QJsonValue(url.toString(QUrl::FullyEncoded)); +} +template<> +QJsonValue toJson(const QByteArray &data) +{ + return QJsonValue(QString::fromLatin1(data.toHex())); +} +template<> +QJsonValue toJson(const QDateTime &datetime) +{ + return QJsonValue(datetime.toString(Qt::ISODate)); +} +template<> +QJsonValue toJson(const QDir &dir) +{ + return QDir::current().relativeFilePath(dir.absolutePath()); +} +template<> +QJsonValue toJson(const QUuid &uuid) +{ + return uuid.toString(); +} +template<> +QJsonValue toJson(const QVariant &variant) +{ + return QJsonValue::fromVariant(variant); +} + + +template<> QByteArray requireIsType(const QJsonValue &value, const QString &what) +{ + const QString string = ensureIsType(value, what); + // ensure that the string can be safely cast to Latin1 + if (string != QString::fromLatin1(string.toLatin1())) + { + throw JsonException(what + " is not encodable as Latin1"); + } + return QByteArray::fromHex(string.toLatin1()); +} + +template<> QJsonArray requireIsType(const QJsonValue &value, const QString &what) +{ + if (!value.isArray()) + { + throw JsonException(what + " is not an array"); + } + return value.toArray(); +} + + +template<> QString requireIsType(const QJsonValue &value, const QString &what) +{ + if (!value.isString()) + { + throw JsonException(what + " is not a string"); + } + return value.toString(); +} + +template<> bool requireIsType(const QJsonValue &value, const QString &what) +{ + if (!value.isBool()) + { + throw JsonException(what + " is not a bool"); + } + return value.toBool(); +} + +template<> double requireIsType(const QJsonValue &value, const QString &what) +{ + if (!value.isDouble()) + { + throw JsonException(what + " is not a double"); + } + return value.toDouble(); +} + +template<> int requireIsType(const QJsonValue &value, const QString &what) +{ + const double doubl = requireIsType(value, what); + if (fmod(doubl, 1) != 0) + { + throw JsonException(what + " is not an integer"); + } + return int(doubl); +} + +template<> QDateTime requireIsType(const QJsonValue &value, const QString &what) +{ + const QString string = requireIsType(value, what); + const QDateTime datetime = QDateTime::fromString(string, Qt::ISODate); + if (!datetime.isValid()) + { + throw JsonException(what + " is not a ISO formatted date/time value"); + } + return datetime; +} + +template<> QUrl requireIsType(const QJsonValue &value, const QString &what) +{ + const QString string = ensureIsType(value, what); + if (string.isEmpty()) + { + return QUrl(); + } + const QUrl url = QUrl(string, QUrl::StrictMode); + if (!url.isValid()) + { + throw JsonException(what + " is not a correctly formatted URL"); + } + return url; +} + +template<> QDir requireIsType(const QJsonValue &value, const QString &what) +{ + const QString string = requireIsType(value, what); + // FIXME: does not handle invalid characters! + return QDir::current().absoluteFilePath(string); +} + +template<> QUuid requireIsType(const QJsonValue &value, const QString &what) +{ + const QString string = requireIsType(value, what); + const QUuid uuid = QUuid(string); + if (uuid.toString() != string) // converts back => valid + { + throw JsonException(what + " is not a valid UUID"); + } + return uuid; +} + +template<> QJsonObject requireIsType(const QJsonValue &value, const QString &what) +{ + if (!value.isObject()) + { + throw JsonException(what + " is not an object"); + } + return value.toObject(); +} + +template<> QVariant requireIsType(const QJsonValue &value, const QString &what) +{ + if (value.isNull() || value.isUndefined()) + { + throw JsonException(what + " is null or undefined"); + } + return value.toVariant(); +} + +template<> QJsonValue requireIsType(const QJsonValue &value, const QString &what) +{ + if (value.isNull() || value.isUndefined()) + { + throw JsonException(what + " is null or undefined"); + } + return value; +} + +} diff --git a/ultimmc/launcher/Json.h b/ultimmc/launcher/Json.h new file mode 100644 index 0000000..359f475 --- /dev/null +++ b/ultimmc/launcher/Json.h @@ -0,0 +1,251 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Exception.h" + +namespace Json +{ +class JsonException : public ::Exception +{ +public: + JsonException(const QString &message) : Exception(message) {} +}; + +/// @throw FileSystemException +void write(const QJsonDocument &doc, const QString &filename); +/// @throw FileSystemException +void write(const QJsonObject &object, const QString &filename); +/// @throw FileSystemException +void write(const QJsonArray &array, const QString &filename); + +QByteArray toBinary(const QJsonObject &obj); +QByteArray toBinary(const QJsonArray &array); +QByteArray toText(const QJsonObject &obj); +QByteArray toText(const QJsonArray &array); + +/// @throw JsonException +QJsonDocument requireDocument(const QByteArray &data, const QString &what = "Document"); +/// @throw JsonException +QJsonDocument requireDocument(const QString &filename, const QString &what = "Document"); +/// @throw JsonException +QJsonObject requireObject(const QJsonDocument &doc, const QString &what = "Document"); +/// @throw JsonException +QJsonObject requireObject(const QJsonValueRef &node, const QString &what = "Node"); +/// @throw JsonException +QJsonArray requireArray(const QJsonDocument &doc, const QString &what = "Document"); + +/////////////////// WRITING //////////////////// + +void writeString(QJsonObject & to, const QString &key, const QString &value); +void writeStringList(QJsonObject & to, const QString &key, const QStringList &values); + +template +QJsonValue toJson(const T &t) +{ + return QJsonValue(t); +} +template<> +QJsonValue toJson(const QUrl &url); +template<> +QJsonValue toJson(const QByteArray &data); +template<> +QJsonValue toJson(const QDateTime &datetime); +template<> +QJsonValue toJson(const QDir &dir); +template<> +QJsonValue toJson(const QUuid &uuid); +template<> +QJsonValue toJson(const QVariant &variant); + +template +QJsonArray toJsonArray(const QList &container) +{ + QJsonArray array; + for (const T item : container) + { + array.append(toJson(item)); + } + return array; +} + +////////////////// READING //////////////////// + +/// @throw JsonException +template +T requireIsType(const QJsonValue &value, const QString &what = "Value"); + +/// @throw JsonException +template<> double requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> bool requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> int requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> QJsonObject requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> QJsonArray requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> QJsonValue requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> QByteArray requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> QDateTime requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> QVariant requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> QString requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> QUuid requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> QDir requireIsType(const QJsonValue &value, const QString &what); +/// @throw JsonException +template<> QUrl requireIsType(const QJsonValue &value, const QString &what); + +// the following functions are higher level functions, that make use of the above functions for +// type conversion +template +T ensureIsType(const QJsonValue &value, const T default_ = T(), const QString &what = "Value") +{ + if (value.isUndefined() || value.isNull()) + { + return default_; + } + try + { + return requireIsType(value, what); + } + catch (const JsonException &) + { + return default_; + } +} + +/// @throw JsonException +template +T requireIsType(const QJsonObject &parent, const QString &key, const QString &what = "__placeholder__") +{ + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) + { + throw JsonException(localWhat + "s parent does not contain " + localWhat); + } + return requireIsType(parent.value(key), localWhat); +} + +template +T ensureIsType(const QJsonObject &parent, const QString &key, const T default_ = T(), const QString &what = "__placeholder__") +{ + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) + { + return default_; + } + return ensureIsType(parent.value(key), default_, localWhat); +} + +template +QVector requireIsArrayOf(const QJsonDocument &doc) +{ + const QJsonArray array = requireArray(doc); + QVector out; + for (const QJsonValue val : array) + { + out.append(requireIsType(val, "Document")); + } + return out; +} + +template +QVector ensureIsArrayOf(const QJsonValue &value, const QString &what = "Value") +{ + const QJsonArray array = ensureIsType(value, QJsonArray(), what); + QVector out; + for (const QJsonValue val : array) + { + out.append(requireIsType(val, what)); + } + return out; +} + +template +QVector ensureIsArrayOf(const QJsonValue &value, const QVector default_, const QString &what = "Value") +{ + if (value.isUndefined()) + { + return default_; + } + return ensureIsArrayOf(value, what); +} + +/// @throw JsonException +template +QVector requireIsArrayOf(const QJsonObject &parent, const QString &key, const QString &what = "__placeholder__") +{ + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) + { + throw JsonException(localWhat + "s parent does not contain " + localWhat); + } + return ensureIsArrayOf(parent.value(key), localWhat); +} + +template +QVector ensureIsArrayOf(const QJsonObject &parent, const QString &key, + const QVector &default_ = QVector(), const QString &what = "__placeholder__") +{ + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) + { + return default_; + } + return ensureIsArrayOf(parent.value(key), default_, localWhat); +} + +// this macro part could be replaced by variadic functions that just pass on their arguments, but that wouldn't work well with IDE helpers +#define JSON_HELPERFUNCTIONS(NAME, TYPE) \ + inline TYPE requireValue##NAME(const QJsonValue &value, const QString &what = "Value") \ + { \ + return requireIsType(value, what); \ + } \ + inline TYPE ensureValue##NAME(const QJsonValue &value, const TYPE default_ = TYPE(), const QString &what = "Value") \ + { \ + return ensureIsType(value, default_, what); \ + } \ + inline TYPE require##NAME(const QJsonObject &parent, const QString &key, const QString &what = "__placeholder__") \ + { \ + return requireIsType(parent, key, what); \ + } \ + inline TYPE ensure##NAME(const QJsonObject &parent, const QString &key, const TYPE default_ = TYPE(), const QString &what = "__placeholder") \ + { \ + return ensureIsType(parent, key, default_, what); \ + } + +JSON_HELPERFUNCTIONS(Array, QJsonArray) +JSON_HELPERFUNCTIONS(Object, QJsonObject) +JSON_HELPERFUNCTIONS(JsonValue, QJsonValue) +JSON_HELPERFUNCTIONS(String, QString) +JSON_HELPERFUNCTIONS(Boolean, bool) +JSON_HELPERFUNCTIONS(Double, double) +JSON_HELPERFUNCTIONS(Integer, int) +JSON_HELPERFUNCTIONS(DateTime, QDateTime) +JSON_HELPERFUNCTIONS(Url, QUrl) +JSON_HELPERFUNCTIONS(ByteArray, QByteArray) +JSON_HELPERFUNCTIONS(Dir, QDir) +JSON_HELPERFUNCTIONS(Uuid, QUuid) +JSON_HELPERFUNCTIONS(Variant, QVariant) + +#undef JSON_HELPERFUNCTIONS + +} +using JSONValidationError = Json::JsonException; diff --git a/ultimmc/launcher/KonamiCode.cpp b/ultimmc/launcher/KonamiCode.cpp new file mode 100644 index 0000000..46a2a0b --- /dev/null +++ b/ultimmc/launcher/KonamiCode.cpp @@ -0,0 +1,44 @@ +#include "KonamiCode.h" + +#include +#include + +namespace { +const std::array konamiCode = +{ + { + Qt::Key_Up, Qt::Key_Up, + Qt::Key_Down, Qt::Key_Down, + Qt::Key_Left, Qt::Key_Right, + Qt::Key_Left, Qt::Key_Right, + Qt::Key_B, Qt::Key_A + } +}; +} + +KonamiCode::KonamiCode(QObject* parent) : QObject(parent) +{ +} + + +void KonamiCode::input(QEvent* event) +{ + if( event->type() == QEvent::KeyPress ) + { + QKeyEvent *keyEvent = static_cast( event ); + auto key = Qt::Key(keyEvent->key()); + if(key == konamiCode[m_progress]) + { + m_progress ++; + } + else + { + m_progress = 0; + } + if(m_progress == static_cast(konamiCode.size())) + { + m_progress = 0; + emit triggered(); + } + } +} diff --git a/ultimmc/launcher/KonamiCode.h b/ultimmc/launcher/KonamiCode.h new file mode 100644 index 0000000..3d320ae --- /dev/null +++ b/ultimmc/launcher/KonamiCode.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +class KonamiCode : public QObject +{ + Q_OBJECT +public: + KonamiCode(QObject *parent = 0); + void input(QEvent *event); + +signals: + void triggered(); + +private: + int m_progress = 0; +}; diff --git a/ultimmc/launcher/LaunchController.cpp b/ultimmc/launcher/LaunchController.cpp new file mode 100644 index 0000000..966ef17 --- /dev/null +++ b/ultimmc/launcher/LaunchController.cpp @@ -0,0 +1,472 @@ +#include "LaunchController.h" +#include "minecraft/auth/AccountList.h" +#include "Application.h" + +#include "ui/MainWindow.h" +#include "ui/InstanceWindow.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProfileSelectDialog.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/EditAccountDialog.h" +#include "ui/dialogs/ProfileSetupDialog.h" +#include "ui/dialogs/LoginDialog.h" +#include "ui/dialogs/MSALoginDialog.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "BuildConfig.h" +#include "JavaCommon.h" +#include "tasks/Task.h" +#include "minecraft/auth/AccountTask.h" +#include "launch/steps/TextPrint.h" + +LaunchController::LaunchController(QObject *parent) : Task(parent) +{ +} + +void LaunchController::executeTask() +{ + if (!m_instance) + { + emitFailed(tr("No instance specified!")); + return; + } + + if(!JavaCommon::checkJVMArgs(m_instance->settings()->get("JvmArgs").toString(), m_parentWidget)) { + emitFailed(tr("Invalid Java arguments specified. Please fix this first.")); + return; + } + + login(); +} + +void LaunchController::decideAccount() +{ + if(m_accountToUse) { + return; + } + + // Find an account to use. + auto accounts = APPLICATION->accounts(); + if (accounts->count() <= 0) + { + // Tell the user they need to log in at least one account in order to play. + auto reply = CustomMessageBox::selectable( + m_parentWidget, + tr("No Accounts"), + tr("In order to play Minecraft, you must have at least one Mojang or Minecraft " + "account logged in." + "Would you like to open the account manager to add an account now?"), + QMessageBox::Information, + QMessageBox::Yes | QMessageBox::No + )->exec(); + + if (reply == QMessageBox::Yes) + { + // Open the account manager. + APPLICATION->ShowGlobalSettings(m_parentWidget, "accounts"); + } + } + + m_accountToUse = accounts->defaultAccount(); + if (!m_accountToUse) + { + // If no default account is set, ask the user which one to use. + ProfileSelectDialog selectDialog( + tr("Which account would you like to use?"), + ProfileSelectDialog::GlobalDefaultCheckbox, + m_parentWidget + ); + + selectDialog.exec(); + + // Launch the instance with the selected account. + m_accountToUse = selectDialog.selectedAccount(); + + // If the user said to use the account as default, do that. + if (selectDialog.useAsGlobalDefault() && m_accountToUse) { + accounts->setDefaultAccount(m_accountToUse); + } + } +} + + +void LaunchController::login() { + decideAccount(); + + // if no account is selected, we bail + if (!m_accountToUse) + { + emitFailed(tr("No account selected for launch.")); + return; + } + + // we try empty password first :) + QString password; + // we loop until the user succeeds in logging in or gives up + bool tryagain = true; + // the failure. the default failure. + const QString needLoginAgain = tr("Your account is currently not logged in. Please enter your password to log in again.

This could be caused by a password change."); + QString failReason = needLoginAgain; + + while (tryagain) + { + m_session = std::make_shared(); + m_session->wants_online = m_online; + m_accountToUse->fillSession(m_session); + + if (m_accountToUse->typeString() == "local" || m_accountToUse->typeString() == "elyby") { + launchInstance(); + return; + } + + switch(m_accountToUse->accountState()) { + case AccountState::Offline: { + m_session->wants_online = false; + // NOTE: fallthrough is intentional + } + case AccountState::Online: { + if(!m_session->wants_online) { + QString usedname; + if(m_offlineName.isEmpty()) { + // we ask the user for a player name + bool ok = false; + QString lastOfflinePlayerName = APPLICATION->settings()->get("LastOfflinePlayerName").toString(); + usedname = lastOfflinePlayerName.isEmpty() ? m_session->player_name : lastOfflinePlayerName; + QString name = QInputDialog::getText( + m_parentWidget, + tr("Player name"), + tr("Choose your offline mode player name."), + QLineEdit::Normal, + usedname, + &ok + ); + if (!ok) + { + tryagain = false; + break; + } + if (name.length()) + { + usedname = name; + APPLICATION->settings()->set("LastOfflinePlayerName", usedname); + } + } + else { + usedname = m_offlineName; + } + + m_session->MakeOffline(usedname); + // offline flavored game from here :3 + } + if(m_accountToUse->ownsMinecraft()) { + if(!m_accountToUse->hasProfile()) { + // Now handle setting up a profile name here... + ProfileSetupDialog dialog(m_accountToUse, m_parentWidget); + if (dialog.exec() == QDialog::Accepted) + { + tryagain = true; + continue; + } + else + { + emitFailed(tr("Received undetermined session status during login.")); + return; + } + } + // we own Minecraft, there is a profile, it's all ready to go! + launchInstance(); + return; + } + else { + // play demo ? + QMessageBox box(m_parentWidget); + box.setWindowTitle(tr("Play demo?")); + box.setText(tr("This account does not own Minecraft.\nYou need to purchase the game first to play it.\n\nDo you want to play the demo?")); + box.setIcon(QMessageBox::Warning); + auto demoButton = box.addButton(tr("Play Demo"), QMessageBox::ButtonRole::YesRole); + auto cancelButton = box.addButton(tr("Cancel"), QMessageBox::ButtonRole::NoRole); + box.setDefaultButton(cancelButton); + + box.exec(); + if(box.clickedButton() == demoButton) { + // play demo here + m_session->MakeDemo(); + launchInstance(); + } + else { + emitFailed(tr("Launch cancelled - account does not own Minecraft.")); + } + } + return; + } + case AccountState::Errored: + // This means some sort of soft error that we can fix with a refresh ... so let's refresh. + case AccountState::Unchecked: { + m_accountToUse->refresh(); + // NOTE: fallthrough intentional + } + case AccountState::Working: { + // refresh is in progress, we need to wait for it to finish to proceed. + ProgressDialog progDialog(m_parentWidget); + if (m_online) + { + progDialog.setSkipButton(true, tr("Play Offline")); + } + auto task = m_accountToUse->currentTask(); + progDialog.execWithTask(task.get()); + continue; + } + // FIXME: this is missing - the meaning is that the account is queued for refresh and we should wait for that + /* + case AccountState::Queued: { + return; + } + */ + case AccountState::Expired: { + auto errorString = tr("The account has expired and needs to be logged into manually. Press OK to log in again."); + auto button = QMessageBox::warning( + m_parentWidget, + tr("Account refresh failed"), + errorString, + QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel, + QMessageBox::StandardButton::Ok + ); + if (button == QMessageBox::StandardButton::Ok) { + auto accounts = APPLICATION->accounts(); + bool isDefault = accounts->defaultAccount() == m_accountToUse; + bool msa = m_accountToUse->isMSA(); + accounts->removeAccount(accounts->index(accounts->findAccountByProfileId(m_accountToUse->profileId()))); + MinecraftAccountPtr newAccount = nullptr; + if (msa) { + if(BuildConfig.BUILD_PLATFORM == "osx64") { + CustomMessageBox::selectable( + m_parentWidget, + tr("Microsoft Accounts not available"), + tr( + "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated MultiMC.\n\n" + "Please update both your operating system and MultiMC." + ), + QMessageBox::Warning + )->exec(); + emitFailed(tr("Attempted to re-login to a Microsoft account on an unsupported platform")); + return; + } + newAccount = MSALoginDialog::newAccount( + m_parentWidget, + tr("Please enter your Mojang account email and password to add your account.") + ); + } else { + newAccount = LoginDialog::newAccount( + m_parentWidget, + tr("Please enter your Mojang account email and password to add your account.") + ); + } + if (newAccount) { + accounts->addAccount(newAccount); + if (isDefault) { + accounts->setDefaultAccount(newAccount); + } + m_accountToUse = nullptr; + decideAccount(); + continue; + } else { + emitFailed(tr("Account expired and re-login attempt failed")); + return; + } + } else { + emitFailed(errorString); + return; + } + } + case AccountState::Gone: { + auto errorString = tr("The account no longer exists on the servers. It may have been migrated, in which case please add the new account you migrated this one to."); + QMessageBox::warning( + m_parentWidget, + tr("Account gone"), + errorString, + QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok + ); + emitFailed(errorString); + return; + } + case AccountState::MustMigrate: { + auto errorString = tr("The account must be migrated to a Microsoft account."); + QMessageBox::warning( + m_parentWidget, + tr("Account requires migration"), + errorString, + QMessageBox::StandardButton::Ok, + QMessageBox::StandardButton::Ok + ); + emitFailed(errorString); + return; + } + } + } + emitFailed(tr("Failed to launch.")); +} + +void LaunchController::launchInstance() +{ + Q_ASSERT_X(m_instance != NULL, "launchInstance", "instance is NULL"); + Q_ASSERT_X(m_session.get() != nullptr, "launchInstance", "session is NULL"); + + if(!m_instance->reloadSettings()) + { + QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't load the instance profile.")); + emitFailed(tr("Couldn't load the instance profile.")); + return; + } + + m_launcher = m_instance->createLaunchTask(m_session, m_quickPlayTarget, m_authserver->port()); + if (!m_launcher) + { + emitFailed(tr("Couldn't instantiate a launcher.")); + return; + } + + auto console = qobject_cast(m_parentWidget); + auto showConsole = m_instance->settings()->get("ShowConsole").toBool(); + if(!console && showConsole) + { + APPLICATION->showInstanceWindow(m_instance); + } + connect(m_launcher.get(), &LaunchTask::readyForLaunch, this, &LaunchController::readyForLaunch); + connect(m_launcher.get(), &LaunchTask::succeeded, this, &LaunchController::onSucceeded); + connect(m_launcher.get(), &LaunchTask::failed, this, &LaunchController::onFailed); + connect(m_launcher.get(), &LaunchTask::requestProgress, this, &LaunchController::onProgressRequested); + + // Prepend Online and Auth Status + QString online_mode; + if(m_session->wants_online) { + online_mode = "online"; + + // Prepend Server Status + QStringList servers = {"authserver.mojang.com", "session.minecraft.net", "textures.minecraft.net", "api.mojang.com"}; + QString resolved_servers = ""; + QHostInfo host_info; + + for(QString server : servers) { + host_info = QHostInfo::fromName(server); + resolved_servers = resolved_servers + server + " resolves to:\n ["; + if(!host_info.addresses().isEmpty()) { + for(QHostAddress address : host_info.addresses()) { + resolved_servers = resolved_servers + address.toString(); + if(!host_info.addresses().endsWith(address)) { + resolved_servers = resolved_servers + ", "; + } + } + } else { + resolved_servers = resolved_servers + "N/A"; + } + resolved_servers = resolved_servers + "]\n\n"; + } + m_launcher->prependStep(new TextPrint(m_launcher.get(), resolved_servers, MessageLevel::Launcher)); + } else { + online_mode = "offline"; + } + + m_launcher->prependStep(new TextPrint(m_launcher.get(), "Launched instance in " + online_mode + " mode\n", MessageLevel::Launcher)); + + // Prepend Version + m_launcher->prependStep(new TextPrint(m_launcher.get(), BuildConfig.LAUNCHER_NAME + " version: " + BuildConfig.printableVersionString() + "\n\n", MessageLevel::Launcher)); + m_launcher->start(); +} + +void LaunchController::readyForLaunch() +{ + if (!m_profiler) + { + m_launcher->proceed(); + return; + } + + QString error; + if (!m_profiler->check(&error)) + { + m_launcher->abort(); + QMessageBox::critical(m_parentWidget, tr("Error!"), tr("Couldn't start profiler: %1").arg(error)); + emitFailed("Profiler startup failed!"); + return; + } + BaseProfiler *profilerInstance = m_profiler->createProfiler(m_launcher->instance(), this); + + connect(profilerInstance, &BaseProfiler::readyToLaunch, [this](const QString & message) + { + QMessageBox msg; + msg.setText(tr("The game launch is delayed until you press the " + "button. This is the right time to setup the profiler, as the " + "profiler server is running now.\n\n%1").arg(message)); + msg.setWindowTitle(tr("Waiting.")); + msg.setIcon(QMessageBox::Information); + msg.addButton(tr("Launch"), QMessageBox::AcceptRole); + msg.setModal(true); + msg.exec(); + m_launcher->proceed(); + }); + connect(profilerInstance, &BaseProfiler::abortLaunch, [this](const QString & message) + { + QMessageBox msg; + msg.setText(tr("Couldn't start the profiler: %1").arg(message)); + msg.setWindowTitle(tr("Error")); + msg.setIcon(QMessageBox::Critical); + msg.addButton(QMessageBox::Ok); + msg.setModal(true); + msg.exec(); + m_launcher->abort(); + emitFailed("Profiler startup failed!"); + }); + profilerInstance->beginProfiling(m_launcher); +} + +void LaunchController::onSucceeded() +{ + emitSucceeded(); +} + +void LaunchController::onFailed(QString reason) +{ + if(m_instance->settings()->get("ShowConsoleOnError").toBool()) + { + APPLICATION->showInstanceWindow(m_instance, "console"); + } + emitFailed(reason); +} + +void LaunchController::onProgressRequested(Task* task) +{ + ProgressDialog progDialog(m_parentWidget); + progDialog.setSkipButton(true, tr("Abort")); + m_launcher->proceed(); + progDialog.execWithTask(task); +} + +bool LaunchController::abort() +{ + if(!m_launcher) + { + return true; + } + if(!m_launcher->canAbort()) + { + return false; + } + auto response = CustomMessageBox::selectable( + m_parentWidget, tr("Kill Minecraft?"), + tr("This can cause the instance to get corrupted and should only be used if Minecraft " + "is frozen for some reason"), + QMessageBox::Question, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes)->exec(); + if (response == QMessageBox::Yes) + { + return m_launcher->abort(); + } + return false; +} diff --git a/ultimmc/launcher/LaunchController.h b/ultimmc/launcher/LaunchController.h new file mode 100644 index 0000000..96cc64b --- /dev/null +++ b/ultimmc/launcher/LaunchController.h @@ -0,0 +1,87 @@ +#pragma once +#include +#include +#include + +#include "minecraft/launch/QuickPlayTarget.h" +#include "minecraft/auth/MinecraftAccount.h" +#include "AuthServer.h" + +class InstanceWindow; +class LaunchController: public Task +{ + Q_OBJECT +public: + void executeTask() override; + + LaunchController(QObject * parent = nullptr); + virtual ~LaunchController(){}; + + void setInstance(InstancePtr instance) { + m_instance = instance; + } + + InstancePtr instance() { + return m_instance; + } + + void setOnline(bool online) { + m_online = online; + } + + void setOfflineName(const QString &offlineName) { + m_offlineName = offlineName; + } + + void setProfiler(BaseProfilerFactory *profiler) { + m_profiler = profiler; + } + + void setParentWidget(QWidget * widget) { + m_parentWidget = widget; + } + + void setQuickPlayTarget(QuickPlayTargetPtr quickPlayTarget) { + m_quickPlayTarget = std::move(quickPlayTarget); + } + + void setAuthserver(std::shared_ptr authserver) { + m_authserver = authserver; + } + + void setAccountToUse(MinecraftAccountPtr accountToUse) { + m_accountToUse = std::move(accountToUse); + } + + QString id() + { + return m_instance->id(); + } + + bool abort() override; + +private: + void login(); + void launchInstance(); + void decideAccount(); + +private slots: + void readyForLaunch(); + + void onSucceeded(); + void onFailed(QString reason); + void onProgressRequested(Task *task); + +private: + BaseProfilerFactory *m_profiler = nullptr; + bool m_online = true; + QString m_offlineName; + InstancePtr m_instance; + QWidget * m_parentWidget = nullptr; + InstanceWindow *m_console = nullptr; + MinecraftAccountPtr m_accountToUse = nullptr; + AuthSessionPtr m_session; + shared_qobject_ptr m_launcher; + QuickPlayTargetPtr m_quickPlayTarget; + std::shared_ptr m_authserver; +}; diff --git a/data/minceraft/UltimMC b/ultimmc/launcher/Launcher.in similarity index 99% rename from data/minceraft/UltimMC rename to ultimmc/launcher/Launcher.in index 4788c5e..b79b276 100755 --- a/data/minceraft/UltimMC +++ b/ultimmc/launcher/Launcher.in @@ -14,7 +14,7 @@ if [[ $EUID -eq 0 ]]; then fi -LAUNCHER_NAME=UltimMC +LAUNCHER_NAME=@Launcher_Name@ LAUNCHER_DIR="$(dirname "$(readlink -f "$0")")" echo "Launcher Dir: ${LAUNCHER_DIR}" diff --git a/ultimmc/launcher/LoggedProcess.cpp b/ultimmc/launcher/LoggedProcess.cpp new file mode 100644 index 0000000..49b411d --- /dev/null +++ b/ultimmc/launcher/LoggedProcess.cpp @@ -0,0 +1,207 @@ +#include "LoggedProcess.h" +#include "MessageLevel.h" +#include + +#include + +LoggedProcess::LoggedProcess(QObject *parent) : QProcess(parent) +{ + // QProcess has a strange interface... let's map a lot of those into a few. + connect(this, &QProcess::readyReadStandardOutput, this, &LoggedProcess::on_stdOut); + connect(this, &QProcess::readyReadStandardError, this, &LoggedProcess::on_stdErr); + connect(this, SIGNAL(finished(int,QProcess::ExitStatus)), SLOT(on_exit(int,QProcess::ExitStatus))); + connect(this, SIGNAL(error(QProcess::ProcessError)), this, SLOT(on_error(QProcess::ProcessError))); + connect(this, &QProcess::stateChanged, this, &LoggedProcess::on_stateChange); +} + +LoggedProcess::~LoggedProcess() +{ + if(m_is_detachable) + { + setProcessState(QProcess::NotRunning); + } +} + +QStringList reprocess(const QByteArray & data, QString & leftover) +{ + QString str = leftover + QString::fromLocal8Bit(data); + + str.remove('\r'); + QStringList lines = str.split("\n"); + leftover = lines.takeLast(); + return lines; +} + +void LoggedProcess::on_stdErr() +{ + auto lines = reprocess(readAllStandardError(), m_err_leftover); + emit log(lines, MessageLevel::StdErr); +} + +void LoggedProcess::on_stdOut() +{ + auto lines = reprocess(readAllStandardOutput(), m_out_leftover); + emit log(lines, MessageLevel::StdOut); +} + +void LoggedProcess::on_exit(int exit_code, QProcess::ExitStatus status) +{ + // save the exit code + m_exit_code = exit_code; + + // Flush console window + if (!m_err_leftover.isEmpty()) + { + emit log({m_err_leftover}, MessageLevel::StdErr); + m_err_leftover.clear(); + } + if (!m_out_leftover.isEmpty()) + { + emit log({m_err_leftover}, MessageLevel::StdOut); + m_out_leftover.clear(); + } + + // based on state, send signals + if (!m_is_aborting) + { + if (status == QProcess::NormalExit) + { + //: Message displayed on instance exit + emit log({tr("Process exited with exit code %1 (0x%2).").arg(exit_code).arg(exit_code, 0, 16)}, MessageLevel::Launcher); + changeState(LoggedProcess::Finished); + } + else + { + //: Message displayed on instance crashed + if(exit_code == -1) + emit log({tr("Process crashed.")}, MessageLevel::Launcher); + else + emit log({tr("Process crashed with exit code %1 (0x%2).").arg(exit_code).arg(exit_code, 0, 16)}, MessageLevel::Launcher); + changeState(LoggedProcess::Crashed); + } + + // Filter out some exit codes, which would only result in erroneous output + // -1, 0, 1 and 255 are usually program generated and don't aid much in debugging + if((exit_code < -1 || exit_code > 1) && (exit_code != 255)) + { + // Gross hack for preserving the **exact bit pattern**, we need to "cast" while ignoring the sign bit + unsigned int u_exit_code = *((unsigned int *) &exit_code); + + std::string statusName; + std::string statusDescription; + bool hasNameOrDescription = Sys::lookupSystemStatusCode(u_exit_code, statusName, statusDescription); + if(hasNameOrDescription) + { + emit log({tr("Below is an analysis of the exit code. THIS MAY BE INCORRECT AND SHOULD BE TAKEN WITH A GRAIN OF SALT!")}, MessageLevel::Launcher); + + if(!statusName.empty()) + { + emit log({tr("System exit code name: %1").arg(QString::fromStdString(statusName))}, MessageLevel::Launcher); + } + + if(!statusDescription.empty()) + { + emit log({tr("System exit code description: %1").arg(QString::fromStdString(statusDescription))}, MessageLevel::Launcher); + } + } + } + + emit log({tr("Please note that usually neither the exit code, nor its description are enough to diagnose issues!")}, MessageLevel::Launcher); + emit log({tr("Always upload the entire log and not just the exit code.")}, MessageLevel::Launcher); + } + else + { + //: Message displayed after the instance exits due to kill request + emit log({tr("Process was killed by user.")}, MessageLevel::Error); + changeState(LoggedProcess::Aborted); + } +} + +void LoggedProcess::on_error(QProcess::ProcessError error) +{ + switch(error) + { + case QProcess::FailedToStart: + { + emit log({tr("The process failed to start.")}, MessageLevel::Fatal); + changeState(LoggedProcess::FailedToStart); + break; + } + // we'll just ignore those... never needed them + case QProcess::Crashed: + case QProcess::ReadError: + case QProcess::Timedout: + case QProcess::UnknownError: + case QProcess::WriteError: + break; + } +} + +void LoggedProcess::kill() +{ + m_is_aborting = true; + QProcess::kill(); +} + +int LoggedProcess::exitCode() const +{ + return m_exit_code; +} + +void LoggedProcess::changeState(LoggedProcess::State state) +{ + if(state == m_state) + return; + m_state = state; + emit stateChanged(m_state); +} + +LoggedProcess::State LoggedProcess::state() const +{ + return m_state; +} + +void LoggedProcess::on_stateChange(QProcess::ProcessState state) +{ + switch(state) + { + case QProcess::NotRunning: + break; // let's not - there are too many that handle this already. + case QProcess::Starting: + { + if(m_state != LoggedProcess::NotRunning) + { + qWarning() << "Wrong state change for process from state" << m_state << "to" << (int) LoggedProcess::Starting; + } + changeState(LoggedProcess::Starting); + return; + } + case QProcess::Running: + { + if(m_state != LoggedProcess::Starting) + { + qWarning() << "Wrong state change for process from state" << m_state << "to" << (int) LoggedProcess::Running; + } + changeState(LoggedProcess::Running); + return; + } + } +} + +#if defined Q_OS_WIN32 +#include +#endif + +qint64 LoggedProcess::processId() const +{ +#ifdef Q_OS_WIN + return pid() ? pid()->dwProcessId : 0; +#else + return pid(); +#endif +} + +void LoggedProcess::setDetachable(bool detachable) +{ + m_is_detachable = detachable; +} diff --git a/ultimmc/launcher/LoggedProcess.h b/ultimmc/launcher/LoggedProcess.h new file mode 100644 index 0000000..e52b8a7 --- /dev/null +++ b/ultimmc/launcher/LoggedProcess.h @@ -0,0 +1,79 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "MessageLevel.h" + +/* + * This is a basic process. + * It has line-based logging support and hides some of the nasty bits. + */ +class LoggedProcess : public QProcess +{ +Q_OBJECT +public: + enum State + { + NotRunning, + Starting, + FailedToStart, + Running, + Finished, + Crashed, + Aborted + }; + +public: + explicit LoggedProcess(QObject* parent = 0); + virtual ~LoggedProcess(); + + State state() const; + int exitCode() const; + qint64 processId() const; + + void setDetachable(bool detachable); + +signals: + void log(QStringList lines, MessageLevel::Enum level); + void stateChanged(LoggedProcess::State state); + +public slots: + /** + * @brief kill the process - equivalent to kill -9 + */ + void kill(); + + +private slots: + void on_stdErr(); + void on_stdOut(); + void on_exit(int exit_code, QProcess::ExitStatus status); + void on_error(QProcess::ProcessError error); + void on_stateChange(QProcess::ProcessState); + +private: + void changeState(LoggedProcess::State state); + +private: + QString m_err_leftover; + QString m_out_leftover; + bool m_killed = false; + State m_state = NotRunning; + int m_exit_code = 0; + bool m_is_aborting = false; + bool m_is_detachable = false; +}; diff --git a/ultimmc/launcher/MMCStrings.cpp b/ultimmc/launcher/MMCStrings.cpp new file mode 100644 index 0000000..dc91c8d --- /dev/null +++ b/ultimmc/launcher/MMCStrings.cpp @@ -0,0 +1,76 @@ +#include "MMCStrings.h" + +/// TAKEN FROM Qt, because it doesn't expose it intelligently +static inline QChar getNextChar(const QString &s, int location) +{ + return (location < s.length()) ? s.at(location) : QChar(); +} + +/// TAKEN FROM Qt, because it doesn't expose it intelligently +int Strings::naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs) +{ + for (int l1 = 0, l2 = 0; l1 <= s1.count() && l2 <= s2.count(); ++l1, ++l2) + { + // skip spaces, tabs and 0's + QChar c1 = getNextChar(s1, l1); + while (c1.isSpace()) + c1 = getNextChar(s1, ++l1); + QChar c2 = getNextChar(s2, l2); + while (c2.isSpace()) + c2 = getNextChar(s2, ++l2); + + if (c1.isDigit() && c2.isDigit()) + { + while (c1.digitValue() == 0) + c1 = getNextChar(s1, ++l1); + while (c2.digitValue() == 0) + c2 = getNextChar(s2, ++l2); + + int lookAheadLocation1 = l1; + int lookAheadLocation2 = l2; + int currentReturnValue = 0; + // find the last digit, setting currentReturnValue as we go if it isn't equal + for (QChar lookAhead1 = c1, lookAhead2 = c2; + (lookAheadLocation1 <= s1.length() && lookAheadLocation2 <= s2.length()); + lookAhead1 = getNextChar(s1, ++lookAheadLocation1), + lookAhead2 = getNextChar(s2, ++lookAheadLocation2)) + { + bool is1ADigit = !lookAhead1.isNull() && lookAhead1.isDigit(); + bool is2ADigit = !lookAhead2.isNull() && lookAhead2.isDigit(); + if (!is1ADigit && !is2ADigit) + break; + if (!is1ADigit) + return -1; + if (!is2ADigit) + return 1; + if (currentReturnValue == 0) + { + if (lookAhead1 < lookAhead2) + { + currentReturnValue = -1; + } + else if (lookAhead1 > lookAhead2) + { + currentReturnValue = 1; + } + } + } + if (currentReturnValue != 0) + return currentReturnValue; + } + if (cs == Qt::CaseInsensitive) + { + if (!c1.isLower()) + c1 = c1.toLower(); + if (!c2.isLower()) + c2 = c2.toLower(); + } + int r = QString::localeAwareCompare(c1, c2); + if (r < 0) + return -1; + if (r > 0) + return 1; + } + // The two strings are the same (02 == 2) so fall back to the normal sort + return QString::compare(s1, s2, cs); +} diff --git a/ultimmc/launcher/MMCStrings.h b/ultimmc/launcher/MMCStrings.h new file mode 100644 index 0000000..48052a0 --- /dev/null +++ b/ultimmc/launcher/MMCStrings.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace Strings +{ + int naturalCompare(const QString &s1, const QString &s2, Qt::CaseSensitivity cs); +} diff --git a/ultimmc/launcher/MMCTime.cpp b/ultimmc/launcher/MMCTime.cpp new file mode 100644 index 0000000..387ecf6 --- /dev/null +++ b/ultimmc/launcher/MMCTime.cpp @@ -0,0 +1,42 @@ +/* + * Copyright 2015 Petr Mrazek + * Copyright 2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +QString Time::prettifyDuration(int64_t duration) { + int seconds = (int) (duration % 60); + duration /= 60; + int minutes = (int) (duration % 60); + duration /= 60; + int hours = (int) (duration % 24); + int days = (int) (duration / 24); + if((hours == 0)&&(days == 0)) + { + return QObject::tr("%1m %2s").arg(minutes).arg(seconds); + } + if (days == 0) + { + return QObject::tr("%1h %2m").arg(hours).arg(minutes); + } + return QObject::tr("%1d %2h %3m").arg(days).arg(hours).arg(minutes); +} + +QString Time::prettifyDurationHours(int64_t duration) { + return QString("%1").arg(duration / 3600.0, 0, 'f', 0); +} diff --git a/ultimmc/launcher/MMCTime.h b/ultimmc/launcher/MMCTime.h new file mode 100644 index 0000000..ae1fa9a --- /dev/null +++ b/ultimmc/launcher/MMCTime.h @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace Time { + +QString prettifyDuration(int64_t duration); +QString prettifyDurationHours(int64_t duration); + +} diff --git a/ultimmc/launcher/MMCZip.cpp b/ultimmc/launcher/MMCZip.cpp new file mode 100644 index 0000000..b25c61e --- /dev/null +++ b/ultimmc/launcher/MMCZip.cpp @@ -0,0 +1,312 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include "MMCZip.h" +#include "FileSystem.h" + +#include + +// ours +bool MMCZip::mergeZipFiles(QuaZip *into, QFileInfo from, QSet &contained, const JlCompress::FilterFunction filter) +{ + QuaZip modZip(from.filePath()); + modZip.open(QuaZip::mdUnzip); + + QuaZipFile fileInsideMod(&modZip); + QuaZipFile zipOutFile(into); + for (bool more = modZip.goToFirstFile(); more; more = modZip.goToNextFile()) + { + QString filename = modZip.getCurrentFileName(); + if (filter && !filter(filename)) + { + qDebug() << "Skipping file " << filename << " from " + << from.fileName() << " - filtered"; + continue; + } + if (contained.contains(filename)) + { + qDebug() << "Skipping already contained file " << filename << " from " + << from.fileName(); + continue; + } + contained.insert(filename); + + if (!fileInsideMod.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to open " << filename << " from " << from.fileName(); + return false; + } + + QuaZipNewInfo info_out(fileInsideMod.getActualFileName()); + + if (!zipOutFile.open(QIODevice::WriteOnly, info_out)) + { + qCritical() << "Failed to open " << filename << " in the jar"; + fileInsideMod.close(); + return false; + } + if (!JlCompress::copyData(fileInsideMod, zipOutFile)) + { + zipOutFile.close(); + fileInsideMod.close(); + qCritical() << "Failed to copy data of " << filename << " into the jar"; + return false; + } + zipOutFile.close(); + fileInsideMod.close(); + } + return true; +} + +// ours +bool MMCZip::createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods) +{ + QuaZip zipOut(targetJarPath); + if (!zipOut.open(QuaZip::mdCreate)) + { + QFile::remove(targetJarPath); + qCritical() << "Failed to open the minecraft.jar for modding"; + return false; + } + // Files already added to the jar. + // These files will be skipped. + QSet addedFiles; + + // Modify the jar + QListIterator i(mods); + i.toBack(); + while (i.hasPrevious()) + { + const Mod &mod = i.previous(); + // do not merge disabled mods. + if (!mod.enabled()) + continue; + if (mod.type() == Mod::MOD_ZIPFILE) + { + if (!mergeZipFiles(&zipOut, mod.filename(), addedFiles)) + { + zipOut.close(); + QFile::remove(targetJarPath); + qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + return false; + } + } + else if (mod.type() == Mod::MOD_SINGLEFILE) + { + // FIXME: buggy - does not work with addedFiles + auto filename = mod.filename(); + if (!JlCompress::compressFile(&zipOut, filename.absoluteFilePath(), filename.fileName())) + { + zipOut.close(); + QFile::remove(targetJarPath); + qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + return false; + } + addedFiles.insert(filename.fileName()); + } + else if (mod.type() == Mod::MOD_FOLDER) + { + // FIXME: buggy - does not work with addedFiles + auto filename = mod.filename(); + QString what_to_zip = filename.absoluteFilePath(); + QDir dir(what_to_zip); + dir.cdUp(); + QString parent_dir = dir.absolutePath(); + if (!JlCompress::compressSubDir(&zipOut, what_to_zip, parent_dir, addedFiles)) + { + zipOut.close(); + QFile::remove(targetJarPath); + qCritical() << "Failed to add" << mod.filename().fileName() << "to the jar."; + return false; + } + qDebug() << "Adding folder " << filename.fileName() << " from " + << filename.absoluteFilePath(); + } + else + { + // Make sure we do not continue launching when something is missing or undefined... + zipOut.close(); + QFile::remove(targetJarPath); + qCritical() << "Failed to add unknown mod type" << mod.filename().fileName() << "to the jar."; + return false; + } + } + + if (!mergeZipFiles(&zipOut, QFileInfo(sourceJarPath), addedFiles, [](const QString key){return !key.contains("META-INF");})) + { + zipOut.close(); + QFile::remove(targetJarPath); + qCritical() << "Failed to insert minecraft.jar contents."; + return false; + } + + // Recompress the jar + zipOut.close(); + if (zipOut.getZipError() != 0) + { + QFile::remove(targetJarPath); + qCritical() << "Failed to finalize minecraft.jar!"; + return false; + } + return true; +} + +// ours +QString MMCZip::findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root) +{ + QuaZipDir rootDir(zip, root); + for(auto fileName: rootDir.entryList(QDir::Files)) + { + if(fileName == what) + return root; + } + for(auto fileName: rootDir.entryList(QDir::Dirs)) + { + QString result = findFolderOfFileInZip(zip, what, root + fileName); + if(!result.isEmpty()) + { + return result; + } + } + return QString(); +} + +// ours +bool MMCZip::findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root) +{ + QuaZipDir rootDir(zip, root); + for(auto fileName: rootDir.entryList(QDir::Files)) + { + if(fileName == what) + { + result.append(root); + return true; + } + } + for(auto fileName: rootDir.entryList(QDir::Dirs)) + { + findFilesInZip(zip, what, result, root + fileName); + } + return !result.isEmpty(); +} + + +// ours +nonstd::optional MMCZip::extractSubDir(QuaZip *zip, const QString & subdir, const QString &target) +{ + QDir directory(target); + QStringList extracted; + + qDebug() << "Extracting subdir" << subdir << "from" << zip->getZipName() << "to" << target; + auto numEntries = zip->getEntriesCount(); + if(numEntries < 0) { + qWarning() << "Failed to enumerate files in archive"; + return nonstd::nullopt; + } + else if(numEntries == 0) { + qDebug() << "Extracting empty archives seems odd..."; + return extracted; + } + else if (!zip->goToFirstFile()) + { + qWarning() << "Failed to seek to first file in zip"; + return nonstd::nullopt; + } + + do + { + QString name = zip->getCurrentFileName(); + if(!name.startsWith(subdir)) + { + continue; + } + name.remove(0, subdir.size()); + QString absFilePath = directory.absoluteFilePath(name); + if(name.isEmpty()) + { + absFilePath += "/"; + } + if (!JlCompress::extractFile(zip, "", absFilePath)) + { + qWarning() << "Failed to extract file" << name << "to" << absFilePath; + JlCompress::removeFile(extracted); + return nonstd::nullopt; + } + extracted.append(absFilePath); + qDebug() << "Extracted file" << name; + } while (zip->goToNextFile()); + return extracted; +} + +// ours +bool MMCZip::extractRelFile(QuaZip *zip, const QString &file, const QString &target) +{ + return JlCompress::extractFile(zip, file, target); +} + +// ours +nonstd::optional MMCZip::extractDir(QString fileCompressed, QString dir) +{ + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) + { + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if(fileInfo.size() == 22) { + return QStringList(); + } + qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; + return nonstd::nullopt; + } + return MMCZip::extractSubDir(&zip, "", dir); +} + +// ours +nonstd::optional MMCZip::extractDir(QString fileCompressed, QString subdir, QString dir) +{ + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) + { + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if(fileInfo.size() == 22) { + return QStringList(); + } + qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError();; + return nonstd::nullopt; + } + return MMCZip::extractSubDir(&zip, subdir, dir); +} + +// ours +bool MMCZip::extractFile(QString fileCompressed, QString file, QString target) +{ + QuaZip zip(fileCompressed); + if (!zip.open(QuaZip::mdUnzip)) + { + // check if this is a minimum size empty zip file... + QFileInfo fileInfo(fileCompressed); + if(fileInfo.size() == 22) { + return true; + } + qWarning() << "Could not open archive for unzipping:" << fileCompressed << "Error:" << zip.getZipError(); + return false; + } + return MMCZip::extractRelFile(&zip, file, target); +} diff --git a/ultimmc/launcher/MMCZip.h b/ultimmc/launcher/MMCZip.h new file mode 100644 index 0000000..9c47fa1 --- /dev/null +++ b/ultimmc/launcher/MMCZip.h @@ -0,0 +1,92 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include "minecraft/mod/Mod.h" +#include + +#include +#include + +namespace MMCZip +{ + + /** + * Merge two zip files, using a filter function + */ + bool mergeZipFiles(QuaZip *into, QFileInfo from, QSet &contained, + const JlCompress::FilterFunction filter = nullptr); + + /** + * take a source jar, add mods to it, resulting in target jar + */ + bool createModdedJar(QString sourceJarPath, QString targetJarPath, const QList& mods); + + /** + * Find a single file in archive by file name (not path) + * + * \return the path prefix where the file is + */ + QString findFolderOfFileInZip(QuaZip * zip, const QString & what, const QString &root = QString("")); + + /** + * Find a multiple files of the same name in archive by file name + * If a file is found in a path, no deeper paths are searched + * + * \return true if anything was found + */ + bool findFilesInZip(QuaZip * zip, const QString & what, QStringList & result, const QString &root = QString()); + + /** + * Extract a subdirectory from an archive + */ + nonstd::optional extractSubDir(QuaZip *zip, const QString & subdir, const QString &target); + + bool extractRelFile(QuaZip *zip, const QString & file, const QString &target); + + /** + * Extract a whole archive. + * + * \param fileCompressed The name of the archive. + * \param dir The directory to extract to, the current directory if left empty. + * \return The list of the full paths of the files extracted, empty on failure. + */ + nonstd::optional extractDir(QString fileCompressed, QString dir); + + /** + * Extract a subdirectory from an archive + * + * \param fileCompressed The name of the archive. + * \param subdir The directory within the archive to extract + * \param dir The directory to extract to, the current directory if left empty. + * \return The list of the full paths of the files extracted, empty on failure. + */ + nonstd::optional extractDir(QString fileCompressed, QString subdir, QString dir); + + /** + * Extract a single file from an archive into a directory + * + * \param fileCompressed The name of the archive. + * \param file The file within the archive to extract + * \param dir The directory to extract to, the current directory if left empty. + * \return true for success or false for failure + */ + bool extractFile(QString fileCompressed, QString file, QString dir); + +} diff --git a/ultimmc/launcher/MessageLevel.cpp b/ultimmc/launcher/MessageLevel.cpp new file mode 100644 index 0000000..135ef46 --- /dev/null +++ b/ultimmc/launcher/MessageLevel.cpp @@ -0,0 +1,36 @@ +#include "MessageLevel.h" + +MessageLevel::Enum MessageLevel::getLevel(const QString& levelName) +{ + if (levelName == "Launcher") + return MessageLevel::Launcher; + else if (levelName == "Debug") + return MessageLevel::Debug; + else if (levelName == "Info") + return MessageLevel::Info; + else if (levelName == "Message") + return MessageLevel::Message; + else if (levelName == "Warning") + return MessageLevel::Warning; + else if (levelName == "Error") + return MessageLevel::Error; + else if (levelName == "Fatal") + return MessageLevel::Fatal; + // Skip PrePost, it's not exposed to !![]! + // Also skip StdErr and StdOut + else + return MessageLevel::Unknown; +} + +MessageLevel::Enum MessageLevel::fromLine(QString &line) +{ + // Level prefix + int endmark = line.indexOf("]!"); + if (line.startsWith("!![") && endmark != -1) + { + auto level = MessageLevel::getLevel(line.left(endmark).mid(3)); + line = line.mid(endmark + 2); + return level; + } + return MessageLevel::Unknown; +} diff --git a/ultimmc/launcher/MessageLevel.h b/ultimmc/launcher/MessageLevel.h new file mode 100644 index 0000000..227ad25 --- /dev/null +++ b/ultimmc/launcher/MessageLevel.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +/** + * @brief the MessageLevel Enum + * defines what level a log message is + */ +namespace MessageLevel +{ +enum Enum +{ + Unknown, /**< No idea what this is or where it came from */ + StdOut, /**< Undetermined stderr messages */ + StdErr, /**< Undetermined stdout messages */ + Launcher, /**< Launcher Messages */ + Debug, /**< Debug Messages */ + Info, /**< Info Messages */ + Message, /**< Standard Messages */ + Warning, /**< Warnings */ + Error, /**< Errors */ + Fatal, /**< Fatal Errors */ +}; +MessageLevel::Enum getLevel(const QString &levelName); + +/* Get message level from a line. Line is modified if it was successful. */ +MessageLevel::Enum fromLine(QString &line); +} diff --git a/ultimmc/launcher/NullInstance.h b/ultimmc/launcher/NullInstance.h new file mode 100644 index 0000000..5ad2100 --- /dev/null +++ b/ultimmc/launcher/NullInstance.h @@ -0,0 +1,79 @@ +#pragma once +#include "BaseInstance.h" +#include "launch/LaunchTask.h" + +class NullInstance: public BaseInstance +{ + Q_OBJECT +public: + NullInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString& rootDir) + :BaseInstance(globalSettings, settings, rootDir) + { + setVersionBroken(true); + } + virtual ~NullInstance() {}; + void saveNow() override + { + } + QString getStatusbarDescription() override + { + return tr("Unknown instance type"); + }; + QSet< QString > traits() const override + { + return {}; + }; + QString instanceConfigFolder() const override + { + return instanceRoot(); + }; + shared_qobject_ptr createLaunchTask(AuthSessionPtr, QuickPlayTargetPtr, quint16) override + { + return nullptr; + } + shared_qobject_ptr< Task > createUpdateTask(Net::Mode mode) override + { + return nullptr; + } + QProcessEnvironment createEnvironment() override + { + return QProcessEnvironment(); + } + QMap getVariables() const override + { + return QMap(); + } + IPathMatcher::Ptr getLogFileMatcher() override + { + return nullptr; + } + QString getLogFileRoot() override + { + return instanceRoot(); + } + QString typeName() const override + { + return "Null"; + } + bool canExport() const override + { + return false; + } + bool canEdit() const override + { + return false; + } + bool canLaunch() const override + { + return false; + } + QStringList verboseDescription(AuthSessionPtr session, QuickPlayTargetPtr quickPlayTarget) override + { + QStringList out; + out << "Null instance - placeholder."; + return out; + } + QString modsRoot() const override { + return QString(); + } +}; diff --git a/ultimmc/launcher/ProblemProvider.h b/ultimmc/launcher/ProblemProvider.h new file mode 100644 index 0000000..cd4745f --- /dev/null +++ b/ultimmc/launcher/ProblemProvider.h @@ -0,0 +1,47 @@ +#pragma once + +enum class ProblemSeverity +{ + None, + Warning, + Error +}; + +struct PatchProblem +{ + ProblemSeverity m_severity; + QString m_description; +}; + +class ProblemProvider +{ +public: + virtual ~ProblemProvider() {}; + virtual const QList getProblems() const = 0; + virtual ProblemSeverity getProblemSeverity() const = 0; +}; + +class ProblemContainer : public ProblemProvider +{ +public: + const QList getProblems() const override + { + return m_problems; + } + ProblemSeverity getProblemSeverity() const override + { + return m_problemSeverity; + } + virtual void addProblem(ProblemSeverity severity, const QString &description) + { + if(severity > m_problemSeverity) + { + m_problemSeverity = severity; + } + m_problems.append({severity, description}); + } + +private: + QList m_problems; + ProblemSeverity m_problemSeverity = ProblemSeverity::None; +}; diff --git a/ultimmc/launcher/QObjectPtr.h b/ultimmc/launcher/QObjectPtr.h new file mode 100644 index 0000000..d52bfe7 --- /dev/null +++ b/ultimmc/launcher/QObjectPtr.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include +#include + +namespace details +{ +struct DeleteQObjectLater +{ + void operator()(QObject *obj) const + { + obj->deleteLater(); + } +}; +} +/** + * A unique pointer class with unique pointer semantics intended for derivates of QObject + * Calls deleteLater() instead of destroying the contained object immediately + */ +template using unique_qobject_ptr = std::unique_ptr; + +/** + * A shared pointer class with shared pointer semantics intended for derivates of QObject + * Calls deleteLater() instead of destroying the contained object immediately + */ +template +class shared_qobject_ptr +{ +public: + shared_qobject_ptr(){} + shared_qobject_ptr(T * wrap) + { + reset(wrap); + } + shared_qobject_ptr(const shared_qobject_ptr& other) + { + m_ptr = other.m_ptr; + } + template + shared_qobject_ptr(const shared_qobject_ptr &other) + { + m_ptr = other.unwrap(); + } + +public: + void reset(T * wrap) + { + using namespace std::placeholders; + m_ptr.reset(wrap, std::bind(&QObject::deleteLater, _1)); + } + void reset(const shared_qobject_ptr &other) + { + m_ptr = other.m_ptr; + } + void reset() + { + m_ptr.reset(); + } + T * get() const + { + return m_ptr.get(); + } + T * operator->() const + { + return m_ptr.get(); + } + T & operator*() const + { + return *m_ptr.get(); + } + operator bool() const + { + return m_ptr.get() != nullptr; + } + const std::shared_ptr unwrap() const + { + return m_ptr; + } + bool operator==(const shared_qobject_ptr& other) const { + return m_ptr == other.m_ptr; + } + bool operator!=(const shared_qobject_ptr& other) const { + return m_ptr != other.m_ptr; + } + +private: + std::shared_ptr m_ptr; +}; diff --git a/ultimmc/launcher/RWStorage.h b/ultimmc/launcher/RWStorage.h new file mode 100644 index 0000000..3028388 --- /dev/null +++ b/ultimmc/launcher/RWStorage.h @@ -0,0 +1,66 @@ +#pragma once +#include +#include +#include +#include + +template +class RWStorage +{ +public: + void add(K key, V value) + { + QWriteLocker l(&lock); + cache[key] = value; + stale_entries.remove(key); + } + V get(K key) + { + QReadLocker l(&lock); + if(cache.contains(key)) + { + return cache[key]; + } + else return V(); + } + bool get(K key, V& value) + { + QReadLocker l(&lock); + if(cache.contains(key)) + { + value = cache[key]; + return true; + } + else return false; + } + bool has(K key) + { + QReadLocker l(&lock); + return cache.contains(key); + } + bool stale(K key) + { + QReadLocker l(&lock); + if(!cache.contains(key)) + return true; + return stale_entries.contains(key); + } + void setStale(K key) + { + QWriteLocker l(&lock); + if(cache.contains(key)) + { + stale_entries.insert(key); + } + } + void clear() + { + QWriteLocker l(&lock); + cache.clear(); + stale_entries.clear(); + } +private: + QReadWriteLock lock; + QMap cache; + QSet stale_entries; +}; diff --git a/ultimmc/launcher/RecursiveFileSystemWatcher.cpp b/ultimmc/launcher/RecursiveFileSystemWatcher.cpp new file mode 100644 index 0000000..b7417cd --- /dev/null +++ b/ultimmc/launcher/RecursiveFileSystemWatcher.cpp @@ -0,0 +1,111 @@ +#include "RecursiveFileSystemWatcher.h" + +#include +#include + +RecursiveFileSystemWatcher::RecursiveFileSystemWatcher(QObject *parent) + : QObject(parent), m_watcher(new QFileSystemWatcher(this)) +{ + connect(m_watcher, &QFileSystemWatcher::fileChanged, this, + &RecursiveFileSystemWatcher::fileChange); + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, + &RecursiveFileSystemWatcher::directoryChange); +} + +void RecursiveFileSystemWatcher::setRootDir(const QDir &root) +{ + bool wasEnabled = m_isEnabled; + disable(); + m_root = root; + setFiles(scanRecursive(m_root)); + if (wasEnabled) + { + enable(); + } +} +void RecursiveFileSystemWatcher::setWatchFiles(const bool watchFiles) +{ + bool wasEnabled = m_isEnabled; + disable(); + m_watchFiles = watchFiles; + if (wasEnabled) + { + enable(); + } +} + +void RecursiveFileSystemWatcher::enable() +{ + if (m_isEnabled) + { + return; + } + Q_ASSERT(m_root != QDir::root()); + addFilesToWatcherRecursive(m_root); + m_isEnabled = true; +} +void RecursiveFileSystemWatcher::disable() +{ + if (!m_isEnabled) + { + return; + } + m_isEnabled = false; + m_watcher->removePaths(m_watcher->files()); + m_watcher->removePaths(m_watcher->directories()); +} + +void RecursiveFileSystemWatcher::setFiles(const QStringList &files) +{ + if (files != m_files) + { + m_files = files; + emit filesChanged(); + } +} + +void RecursiveFileSystemWatcher::addFilesToWatcherRecursive(const QDir &dir) +{ + m_watcher->addPath(dir.absolutePath()); + for (const QString &directory : dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) + { + addFilesToWatcherRecursive(dir.absoluteFilePath(directory)); + } + if (m_watchFiles) + { + for (const QFileInfo &info : dir.entryInfoList(QDir::Files)) + { + m_watcher->addPath(info.absoluteFilePath()); + } + } +} +QStringList RecursiveFileSystemWatcher::scanRecursive(const QDir &directory) +{ + QStringList ret; + if(!m_matcher) + { + return {}; + } + for (const QString &dir : directory.entryList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::Hidden)) + { + ret.append(scanRecursive(directory.absoluteFilePath(dir))); + } + for (const QString &file : directory.entryList(QDir::Files | QDir::Hidden)) + { + auto relPath = m_root.relativeFilePath(directory.absoluteFilePath(file)); + if (m_matcher->matches(relPath)) + { + ret.append(relPath); + } + } + return ret; +} + +void RecursiveFileSystemWatcher::fileChange(const QString &path) +{ + emit fileChanged(path); +} +void RecursiveFileSystemWatcher::directoryChange(const QString &path) +{ + setFiles(scanRecursive(m_root)); +} diff --git a/ultimmc/launcher/RecursiveFileSystemWatcher.h b/ultimmc/launcher/RecursiveFileSystemWatcher.h new file mode 100644 index 0000000..cc837d6 --- /dev/null +++ b/ultimmc/launcher/RecursiveFileSystemWatcher.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include "pathmatcher/IPathMatcher.h" + +class RecursiveFileSystemWatcher : public QObject +{ + Q_OBJECT +public: + RecursiveFileSystemWatcher(QObject *parent); + + void setRootDir(const QDir &root); + QDir rootDir() const + { + return m_root; + } + + // WARNING: setting this to true may be bad for performance + void setWatchFiles(const bool watchFiles); + bool watchFiles() const + { + return m_watchFiles; + } + + void setMatcher(IPathMatcher::Ptr matcher) + { + m_matcher = matcher; + } + + QStringList files() const + { + return m_files; + } + +signals: + void filesChanged(); + void fileChanged(const QString &path); + +public slots: + void enable(); + void disable(); + +private: + QDir m_root; + bool m_watchFiles = false; + bool m_isEnabled = false; + IPathMatcher::Ptr m_matcher; + + QFileSystemWatcher *m_watcher; + + QStringList m_files; + void setFiles(const QStringList &files); + + void addFilesToWatcherRecursive(const QDir &dir); + QStringList scanRecursive(const QDir &dir); + +private slots: + void fileChange(const QString &path); + void directoryChange(const QString &path); +}; diff --git a/ultimmc/launcher/SeparatorPrefixTree.h b/ultimmc/launcher/SeparatorPrefixTree.h new file mode 100644 index 0000000..7a841cb --- /dev/null +++ b/ultimmc/launcher/SeparatorPrefixTree.h @@ -0,0 +1,298 @@ +#pragma once +#include +#include +#include + +template +class SeparatorPrefixTree +{ +public: + SeparatorPrefixTree(QStringList paths) + { + insert(paths); + } + + SeparatorPrefixTree(bool contained = false) + { + m_contained = contained; + } + + void insert(QStringList paths) + { + for(auto &path: paths) + { + insert(path); + } + } + + /// insert an exact path into the tree + SeparatorPrefixTree & insert(QString path) + { + auto sepIndex = path.indexOf(Tseparator); + if(sepIndex == -1) + { + children[path] = SeparatorPrefixTree(true); + return children[path]; + } + else + { + auto prefix = path.left(sepIndex); + if(!children.contains(prefix)) + { + children[prefix] = SeparatorPrefixTree(false); + } + return children[prefix].insert(path.mid(sepIndex + 1)); + } + } + + /// is the path fully contained in the tree? + bool contains(QString path) const + { + auto node = find(path); + return node != nullptr; + } + + /// does the tree cover a path? That means the prefix of the path is contained in the tree + bool covers(QString path) const + { + // if we found some valid node, it's good enough. the tree covers the path + if(m_contained) + { + return true; + } + auto sepIndex = path.indexOf(Tseparator); + if(sepIndex == -1) + { + auto found = children.find(path); + if(found == children.end()) + { + return false; + } + return (*found).covers(QString()); + } + else + { + auto prefix = path.left(sepIndex); + auto found = children.find(prefix); + if(found == children.end()) + { + return false; + } + return (*found).covers(path.mid(sepIndex + 1)); + } + } + + /// return the contained path that covers the path specified + QString cover(QString path) const + { + // if we found some valid node, it's good enough. the tree covers the path + if(m_contained) + { + return QString(""); + } + auto sepIndex = path.indexOf(Tseparator); + if(sepIndex == -1) + { + auto found = children.find(path); + if(found == children.end()) + { + return QString(); + } + auto nested = (*found).cover(QString()); + if(nested.isNull()) + { + return nested; + } + if(nested.isEmpty()) + return path; + return path + Tseparator + nested; + } + else + { + auto prefix = path.left(sepIndex); + auto found = children.find(prefix); + if(found == children.end()) + { + return QString(); + } + auto nested = (*found).cover(path.mid(sepIndex + 1)); + if(nested.isNull()) + { + return nested; + } + if(nested.isEmpty()) + return prefix; + return prefix + Tseparator + nested; + } + } + + /// Does the path-specified node exist in the tree? It does not have to be contained. + bool exists(QString path) const + { + auto sepIndex = path.indexOf(Tseparator); + if(sepIndex == -1) + { + auto found = children.find(path); + if(found == children.end()) + { + return false; + } + return true; + } + else + { + auto prefix = path.left(sepIndex); + auto found = children.find(prefix); + if(found == children.end()) + { + return false; + } + return (*found).exists(path.mid(sepIndex + 1)); + } + } + + /// find a node in the tree by name + const SeparatorPrefixTree * find(QString path) const + { + auto sepIndex = path.indexOf(Tseparator); + if(sepIndex == -1) + { + auto found = children.find(path); + if(found == children.end()) + { + return nullptr; + } + return &(*found); + } + else + { + auto prefix = path.left(sepIndex); + auto found = children.find(prefix); + if(found == children.end()) + { + return nullptr; + } + return (*found).find(path.mid(sepIndex + 1)); + } + } + + /// is this a leaf node? + bool leaf() const + { + return children.isEmpty(); + } + + /// is this node actually contained in the tree, or is it purely structural? + bool contained() const + { + return m_contained; + } + + /// Remove a path from the tree + bool remove(QString path) + { + return removeInternal(path) != Failed; + } + + /// Clear all children of this node tree node + void clear() + { + children.clear(); + } + + QStringList toStringList() const + { + QStringList collected; + // collecting these is more expensive. + auto iter = children.begin(); + while(iter != children.end()) + { + QStringList list = iter.value().toStringList(); + for(int i = 0; i < list.size(); i++) + { + list[i] = iter.key() + Tseparator + list[i]; + } + collected.append(list); + if((*iter).m_contained) + { + collected.append(iter.key()); + } + iter++; + } + return collected; + } +private: + enum Removal + { + Failed, + Succeeded, + HasChildren + }; + Removal removeInternal(QString path = QString()) + { + if(path.isEmpty()) + { + if(!m_contained) + { + // remove all children - we are removing a prefix + clear(); + return Succeeded; + } + m_contained = false; + if(children.size()) + { + return HasChildren; + } + return Succeeded; + } + Removal remStatus = Failed; + QString childToRemove; + auto sepIndex = path.indexOf(Tseparator); + if(sepIndex == -1) + { + childToRemove = path; + auto found = children.find(childToRemove); + if(found == children.end()) + { + return Failed; + } + remStatus = (*found).removeInternal(); + } + else + { + childToRemove = path.left(sepIndex); + auto found = children.find(childToRemove); + if(found == children.end()) + { + return Failed; + } + remStatus = (*found).removeInternal(path.mid(sepIndex + 1)); + } + switch (remStatus) + { + case Failed: + case HasChildren: + { + return remStatus; + } + case Succeeded: + { + children.remove(childToRemove); + if(m_contained) + { + return HasChildren; + } + if(children.size()) + { + return HasChildren; + } + return Succeeded; + } + } + return Failed; + } + +private: + QMap> children; + bool m_contained = false; +}; diff --git a/ultimmc/launcher/SkinUtils.cpp b/ultimmc/launcher/SkinUtils.cpp new file mode 100644 index 0000000..1fe0c89 --- /dev/null +++ b/ultimmc/launcher/SkinUtils.cpp @@ -0,0 +1,50 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SkinUtils.h" +#include "net/HttpMetaCache.h" +#include "Application.h" + +#include +#include +#include +#include +#include + +namespace SkinUtils +{ +/* + * Given a username, return a pixmap of the cached skin (if it exists), QPixmap() otherwise + */ +QPixmap getFaceFromCache(QString username, int height, int width) +{ + QFile fskin(APPLICATION->metacache()->resolveEntry("skins", username + ".png")->getFullPath()); + + if (fskin.exists()) + { + QPixmap skinTexture(fskin.fileName()); + if(!skinTexture.isNull()) + { + QPixmap skin = QPixmap(8, 8); + QPainter painter(&skin); + painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); + painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); + return skin.scaled(height, width, Qt::KeepAspectRatio); + } + } + + return QPixmap(); +} +} diff --git a/ultimmc/launcher/SkinUtils.h b/ultimmc/launcher/SkinUtils.h new file mode 100644 index 0000000..c1f437a --- /dev/null +++ b/ultimmc/launcher/SkinUtils.h @@ -0,0 +1,23 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace SkinUtils +{ +QPixmap getFaceFromCache(QString id, int height = 64, int width = 64); +} diff --git a/ultimmc/launcher/UpdateController.cpp b/ultimmc/launcher/UpdateController.cpp new file mode 100644 index 0000000..f9b7d34 --- /dev/null +++ b/ultimmc/launcher/UpdateController.cpp @@ -0,0 +1,457 @@ +#include +#include +#include +#include +#include "UpdateController.h" +#include +#include +#include +#include + +#include "BuildConfig.h" + + +// from +#ifndef S_IRUSR +#define __S_IREAD 0400 /* Read by owner. */ +#define __S_IWRITE 0200 /* Write by owner. */ +#define __S_IEXEC 0100 /* Execute by owner. */ +#define S_IRUSR __S_IREAD /* Read by owner. */ +#define S_IWUSR __S_IWRITE /* Write by owner. */ +#define S_IXUSR __S_IEXEC /* Execute by owner. */ + +#define S_IRGRP (S_IRUSR >> 3) /* Read by group. */ +#define S_IWGRP (S_IWUSR >> 3) /* Write by group. */ +#define S_IXGRP (S_IXUSR >> 3) /* Execute by group. */ + +#define S_IROTH (S_IRGRP >> 3) /* Read by others. */ +#define S_IWOTH (S_IWGRP >> 3) /* Write by others. */ +#define S_IXOTH (S_IXGRP >> 3) /* Execute by others. */ +#endif +static QFile::Permissions unixModeToPermissions(const int mode) +{ + QFile::Permissions perms; + + if (mode & S_IRUSR) + { + perms |= QFile::ReadUser; + } + if (mode & S_IWUSR) + { + perms |= QFile::WriteUser; + } + if (mode & S_IXUSR) + { + perms |= QFile::ExeUser; + } + + if (mode & S_IRGRP) + { + perms |= QFile::ReadGroup; + } + if (mode & S_IWGRP) + { + perms |= QFile::WriteGroup; + } + if (mode & S_IXGRP) + { + perms |= QFile::ExeGroup; + } + + if (mode & S_IROTH) + { + perms |= QFile::ReadOther; + } + if (mode & S_IWOTH) + { + perms |= QFile::WriteOther; + } + if (mode & S_IXOTH) + { + perms |= QFile::ExeOther; + } + return perms; +} + +static const QLatin1String liveCheckFile("live.check"); + +UpdateController::UpdateController(QWidget * parent, const QString& root, const QString updateFilesDir, GoUpdate::OperationList operations) +{ + m_parent = parent; + m_root = root; + m_updateFilesDir = updateFilesDir; + m_operations = operations; +} + + +void UpdateController::installUpdates() +{ + qint64 pid = -1; + QStringList args; + bool started = false; + + qDebug() << "Installing updates."; +#ifdef Q_OS_WIN + QString finishCmd = QApplication::applicationFilePath(); +#elif defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + QString finishCmd = FS::PathCombine(m_root, BuildConfig.LAUNCHER_NAME); +#elif defined Q_OS_MAC + QString finishCmd = QApplication::applicationFilePath(); +#else +#error Unsupported operating system. +#endif + + QString backupPath = FS::PathCombine(m_root, "update", "backup"); + QDir origin(m_root); + + // clean up the backup folder. it should be empty before we start + if(!FS::deletePath(backupPath)) + { + qWarning() << "couldn't remove previous backup folder" << backupPath; + } + // and it should exist. + if(!FS::ensureFolderPathExists(backupPath)) + { + qWarning() << "couldn't create folder" << backupPath; + return; + } + + bool useXPHack = false; + QString exePath; + QString exeOrigin; + QString exeBackup; + + // perform the update operations + for(auto op: m_operations) + { + switch(op.type) + { + // replace = move original out to backup, if it exists, move the new file in its place + case GoUpdate::Operation::OP_REPLACE: + { +#ifdef Q_OS_WIN32 + QString windowsExeName = BuildConfig.LAUNCHER_NAME + ".exe"; + // hack for people renaming the .exe because ... reasons :) + if(op.destination == windowsExeName) + { + op.destination = QFileInfo(QApplication::applicationFilePath()).fileName(); + } +#endif + QFileInfo destination (FS::PathCombine(m_root, op.destination)); +#ifdef Q_OS_WIN32 + if(QSysInfo::windowsVersion() < QSysInfo::WV_VISTA) + { + if(destination.fileName() == windowsExeName) + { + QDir rootDir(m_root); + exeOrigin = rootDir.relativeFilePath(op.source); + exePath = rootDir.relativeFilePath(op.destination); + exeBackup = rootDir.relativeFilePath(FS::PathCombine(backupPath, destination.fileName())); + useXPHack = true; + continue; + } + } +#endif + if(destination.exists()) + { + QString backupName = op.destination; + backupName.replace('/', '_'); + QString backupFilePath = FS::PathCombine(backupPath, backupName); + if(!QFile::rename(destination.absoluteFilePath(), backupFilePath)) + { + qWarning() << "Couldn't move:" << destination.absoluteFilePath() << "to" << backupFilePath; + m_failedOperationType = Replace; + m_failedFile = op.destination; + fail(); + return; + } + BackupEntry be; + be.original = destination.absoluteFilePath(); + be.backup = backupFilePath; + be.update = op.source; + m_replace_backups.append(be); + } + // make sure the folder we are putting this into exists + if(!FS::ensureFilePathExists(destination.absoluteFilePath())) + { + qWarning() << "REPLACE: Couldn't create folder:" << destination.absoluteFilePath(); + m_failedOperationType = Replace; + m_failedFile = op.destination; + fail(); + return; + } + // now move the new file in + if(!QFile::rename(op.source, destination.absoluteFilePath())) + { + qWarning() << "REPLACE: Couldn't move:" << op.source << "to" << destination.absoluteFilePath(); + m_failedOperationType = Replace; + m_failedFile = op.destination; + fail(); + return; + } + QFile::setPermissions(destination.absoluteFilePath(), unixModeToPermissions(op.destinationMode)); + } + break; + // delete = move original to backup + case GoUpdate::Operation::OP_DELETE: + { + QString destFilePath = FS::PathCombine(m_root, op.destination); + if(QFile::exists(destFilePath)) + { + QString backupName = op.destination; + backupName.replace('/', '_'); + QString trashFilePath = FS::PathCombine(backupPath, backupName); + + if(!QFile::rename(destFilePath, trashFilePath)) + { + qWarning() << "DELETE: Couldn't move:" << op.destination << "to" << trashFilePath; + m_failedFile = op.destination; + m_failedOperationType = Delete; + fail(); + return; + } + BackupEntry be; + be.original = destFilePath; + be.backup = trashFilePath; + m_delete_backups.append(be); + } + } + break; + } + } + + // try to start the new binary + args = qApp->arguments(); + args.removeFirst(); + + // on old Windows, do insane things... no error checking here, this is just to have something. + if(useXPHack) + { + QString script; + auto nativePath = QDir::toNativeSeparators(exePath); + auto nativeOriginPath = QDir::toNativeSeparators(exeOrigin); + auto nativeBackupPath = QDir::toNativeSeparators(exeBackup); + + // so we write this vbscript thing... + QTextStream out(&script); + out << "WScript.Sleep 1000\n"; + out << "Set fso=CreateObject(\"Scripting.FileSystemObject\")\n"; + out << "Set shell=CreateObject(\"WScript.Shell\")\n"; + out << "fso.MoveFile \"" << nativePath << "\", \"" << nativeBackupPath << "\"\n"; + out << "fso.MoveFile \"" << nativeOriginPath << "\", \"" << nativePath << "\"\n"; + out << "shell.Run \"" << nativePath << "\"\n"; + + QString scriptPath = FS::PathCombine(m_root, "update", "update.vbs"); + + // we save it + QFile scriptFile(scriptPath); + scriptFile.open(QIODevice::WriteOnly); + scriptFile.write(script.toLocal8Bit().replace("\n", "\r\n")); + scriptFile.close(); + + // we run it + started = QProcess::startDetached("wscript", {scriptPath}, m_root); + + // and we quit. conscious thought. + qApp->quit(); + return; + } + bool doLiveCheck = true; + bool startFailed = false; + + // remove live check file, if any + if(QFile::exists(liveCheckFile)) + { + if(!QFile::remove(liveCheckFile)) + { + qWarning() << "Couldn't remove the" << liveCheckFile << "file! We will proceed without :("; + doLiveCheck = false; + } + } + + if(doLiveCheck) + { + if(!args.contains("--alive")) + { + args.append("--alive"); + } + } + + // FIXME: reparse args and construct a safe variant from scratch. This is a workaround for GH-1874: + QStringList realargs; + int skip = 0; + for(auto & arg: args) + { + if(skip) + { + skip--; + continue; + } + if(arg == "-l") + { + skip = 1; + continue; + } + realargs.append(arg); + } + + // start the updated application + started = QProcess::startDetached(finishCmd, realargs, QDir::currentPath(), &pid); + // much dumber check - just find out if the call + if(!started || pid == -1) + { + qWarning() << "Couldn't start new process properly!"; + startFailed = true; + } + if(!startFailed && doLiveCheck) + { + int attempts = 0; + while(attempts < 10) + { + attempts++; + QString key; + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + if(!QFile::exists(liveCheckFile)) + { + qWarning() << "Couldn't find the" << liveCheckFile << "file!"; + startFailed = true; + continue; + } + try + { + key = QString::fromUtf8(FS::read(liveCheckFile)); + auto id = ApplicationId::fromRawString(key); + LocalPeer peer(nullptr, id); + if(peer.isClient()) + { + startFailed = false; + qDebug() << "Found process started with key " << key; + break; + } + else + { + startFailed = true; + qDebug() << "Process started with key " << key << "apparently died or is not reponding..."; + break; + } + } + catch (const Exception &e) + { + qWarning() << "Couldn't read the" << liveCheckFile << "file!"; + startFailed = true; + continue; + } + } + } + if(startFailed) + { + m_failedOperationType = Start; + fail(); + return; + } + else + { + origin.rmdir(m_updateFilesDir); + qApp->quit(); + return; + } +} + +void UpdateController::fail() +{ + qWarning() << "Update failed!"; + + QString msg; + bool doRollback = false; + QString failTitle = QObject::tr("Update failed!"); + QString rollFailTitle = QObject::tr("Rollback failed!"); + switch (m_failedOperationType) + { + case Replace: + { + msg = QObject::tr( + "Couldn't replace file %1. Changes will be reverted.\n" + "See the %2 log file for details." + ).arg(m_failedFile, BuildConfig.LAUNCHER_NAME); + doRollback = true; + QMessageBox::critical(m_parent, failTitle, msg); + break; + } + case Delete: + { + msg = QObject::tr( + "Couldn't remove file %1. Changes will be reverted.\n" + "See the %2 log file for details." + ).arg(m_failedFile, BuildConfig.LAUNCHER_NAME); + doRollback = true; + QMessageBox::critical(m_parent, failTitle, msg); + break; + } + case Start: + { + msg = QObject::tr("The new version didn't start or is too old and doesn't respond to startup checks.\n" + "\n" + "Roll back to previous version?"); + auto result = QMessageBox::critical( + m_parent, + failTitle, + msg, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes + ); + doRollback = (result == QMessageBox::Yes); + break; + } + case Nothing: + default: + return; + } + if(doRollback) + { + auto rollbackOK = rollback(); + if(!rollbackOK) + { + msg = QObject::tr("The rollback failed too.\n" + "You will have to repair %1 manually.\n" + "Please let us know why and how this happened.").arg(BuildConfig.LAUNCHER_NAME); + QMessageBox::critical(m_parent, rollFailTitle, msg); + qApp->quit(); + } + } + else + { + qApp->quit(); + } +} + +bool UpdateController::rollback() +{ + bool revertOK = true; + // if the above failed, roll back changes + for(auto backup:m_replace_backups) + { + qWarning() << "restoring" << backup.original << "from" << backup.backup; + if(!QFile::rename(backup.original, backup.update)) + { + revertOK = false; + qWarning() << "moving new" << backup.original << "back to" << backup.update << "failed!"; + continue; + } + + if(!QFile::rename(backup.backup, backup.original)) + { + revertOK = false; + qWarning() << "restoring" << backup.original << "failed!"; + } + } + for(auto backup:m_delete_backups) + { + qWarning() << "restoring" << backup.original << "from" << backup.backup; + if(!QFile::rename(backup.backup, backup.original)) + { + revertOK = false; + qWarning() << "restoring" << backup.original << "failed!"; + } + } + return revertOK; +} diff --git a/ultimmc/launcher/UpdateController.h b/ultimmc/launcher/UpdateController.h new file mode 100644 index 0000000..715554e --- /dev/null +++ b/ultimmc/launcher/UpdateController.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +class QWidget; + +class UpdateController +{ +public: + UpdateController(QWidget * parent, const QString &root, const QString updateFilesDir, GoUpdate::OperationList operations); + void installUpdates(); + +private: + void fail(); + bool rollback(); + +private: + QString m_root; + QString m_updateFilesDir; + GoUpdate::OperationList m_operations; + QWidget * m_parent; + + struct BackupEntry + { + // path where we got the new file from + QString update; + // path of what is being actually updated + QString original; + // path where the backup of the updated file was placed + QString backup; + }; + QList m_replace_backups; + QList m_delete_backups; + enum Failure + { + Replace, + Delete, + Start, + Nothing + } m_failedOperationType = Nothing; + QString m_failedFile; +}; diff --git a/ultimmc/launcher/Usable.h b/ultimmc/launcher/Usable.h new file mode 100644 index 0000000..a3e880f --- /dev/null +++ b/ultimmc/launcher/Usable.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include + +#include "QObjectPtr.h" + +class Usable; + +/** + * Base class for things that can be used by multiple other things and we want to track the use count. + * + * @see UseLock + */ +class Usable +{ + friend class UseLock; +public: + std::size_t useCount() const + { + return m_useCount; + } + bool isInUse() const + { + return m_useCount > 0; + } +protected: + virtual void decrementUses() + { + m_useCount--; + } + virtual void incrementUses() + { + m_useCount++; + } +private: + std::size_t m_useCount = 0; +}; + +/** + * Lock class to use for keeping track of uses of other things derived from Usable + * + * @see Usable + */ +class UseLock +{ +public: + UseLock(shared_qobject_ptr usable) + : m_usable(usable) + { + // this doesn't use shared pointer use count, because that wouldn't be correct. this count is separate. + m_usable->incrementUses(); + } + ~UseLock() + { + m_usable->decrementUses(); + } +private: + shared_qobject_ptr m_usable; +}; diff --git a/ultimmc/launcher/Version.cpp b/ultimmc/launcher/Version.cpp new file mode 100644 index 0000000..b9090e2 --- /dev/null +++ b/ultimmc/launcher/Version.cpp @@ -0,0 +1,85 @@ +#include "Version.h" + +#include +#include +#include +#include + +Version::Version(const QString &str) : m_string(str) +{ + parse(); +} + +bool Version::operator<(const Version &other) const +{ + const int size = qMax(m_sections.size(), other.m_sections.size()); + for (int i = 0; i < size; ++i) + { + const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i); + const Section sec2 = + (i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i); + if (sec1 != sec2) + { + return sec1 < sec2; + } + } + + return false; +} +bool Version::operator<=(const Version &other) const +{ + return *this < other || *this == other; +} +bool Version::operator>(const Version &other) const +{ + const int size = qMax(m_sections.size(), other.m_sections.size()); + for (int i = 0; i < size; ++i) + { + const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i); + const Section sec2 = + (i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i); + if (sec1 != sec2) + { + return sec1 > sec2; + } + } + + return false; +} +bool Version::operator>=(const Version &other) const +{ + return *this > other || *this == other; +} +bool Version::operator==(const Version &other) const +{ + const int size = qMax(m_sections.size(), other.m_sections.size()); + for (int i = 0; i < size; ++i) + { + const Section sec1 = (i >= m_sections.size()) ? Section("0") : m_sections.at(i); + const Section sec2 = + (i >= other.m_sections.size()) ? Section("0") : other.m_sections.at(i); + if (sec1 != sec2) + { + return false; + } + } + + return true; +} +bool Version::operator!=(const Version &other) const +{ + return !operator==(other); +} + +void Version::parse() +{ + m_sections.clear(); + + // FIXME: this is bad. versions can contain a lot more separators... + QStringList parts = m_string.split('.'); + + for (const auto& part : parts) + { + m_sections.append(Section(part)); + } +} diff --git a/ultimmc/launcher/Version.h b/ultimmc/launcher/Version.h new file mode 100644 index 0000000..9fe12d6 --- /dev/null +++ b/ultimmc/launcher/Version.h @@ -0,0 +1,105 @@ +#pragma once + +#include +#include + +class QUrl; + +class Version +{ +public: + Version(const QString &str); + Version() {} + + bool operator<(const Version &other) const; + bool operator<=(const Version &other) const; + bool operator>(const Version &other) const; + bool operator>=(const Version &other) const; + bool operator==(const Version &other) const; + bool operator!=(const Version &other) const; + + QString toString() const + { + return m_string; + } + +private: + QString m_string; + struct Section + { + explicit Section(const QString &fullString) + { + m_fullString = fullString; + int cutoff = m_fullString.size(); + for(int i = 0; i < m_fullString.size(); i++) + { + if(!m_fullString[i].isDigit()) + { + cutoff = i; + break; + } + } + auto numPart = m_fullString.leftRef(cutoff); + if(numPart.size()) + { + numValid = true; + m_numPart = numPart.toInt(); + } + auto stringPart = m_fullString.midRef(cutoff); + if(stringPart.size()) + { + m_stringPart = stringPart.toString(); + } + } + explicit Section() {} + bool numValid = false; + int m_numPart = 0; + QString m_stringPart; + QString m_fullString; + + inline bool operator!=(const Section &other) const + { + if(numValid && other.numValid) + { + return m_numPart != other.m_numPart || m_stringPart != other.m_stringPart; + } + else + { + return m_fullString != other.m_fullString; + } + } + inline bool operator<(const Section &other) const + { + if(numValid && other.numValid) + { + if(m_numPart < other.m_numPart) + return true; + if(m_numPart == other.m_numPart && m_stringPart < other.m_stringPart) + return true; + return false; + } + else + { + return m_fullString < other.m_fullString; + } + } + inline bool operator>(const Section &other) const + { + if(numValid && other.numValid) + { + if(m_numPart > other.m_numPart) + return true; + if(m_numPart == other.m_numPart && m_stringPart > other.m_stringPart) + return true; + return false; + } + else + { + return m_fullString > other.m_fullString; + } + } + }; + QList
m_sections; + + void parse(); +}; diff --git a/ultimmc/launcher/VersionProxyModel.cpp b/ultimmc/launcher/VersionProxyModel.cpp new file mode 100644 index 0000000..b9a87c9 --- /dev/null +++ b/ultimmc/launcher/VersionProxyModel.cpp @@ -0,0 +1,447 @@ +#include "VersionProxyModel.h" +#include "Application.h" +#include +#include +#include +#include + +class VersionFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + VersionFilterModel(VersionProxyModel *parent) : QSortFilterProxyModel(parent) + { + m_parent = parent; + setSortRole(BaseVersionList::SortRole); + sort(0, Qt::DescendingOrder); + } + + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const + { + const auto &filters = m_parent->filters(); + for (auto it = filters.begin(); it != filters.end(); ++it) + { + auto idx = sourceModel()->index(source_row, 0, source_parent); + auto data = sourceModel()->data(idx, it.key()); + auto match = data.toString(); + if(!it.value()->accepts(match)) + { + return false; + } + } + return true; + } + + void filterChanged() + { + invalidateFilter(); + } +private: + VersionProxyModel *m_parent; +}; + +VersionProxyModel::VersionProxyModel(QObject *parent) : QAbstractProxyModel(parent) +{ + filterModel = new VersionFilterModel(this); + connect(filterModel, &QAbstractItemModel::dataChanged, this, &VersionProxyModel::sourceDataChanged); + connect(filterModel, &QAbstractItemModel::rowsAboutToBeInserted, this, &VersionProxyModel::sourceRowsAboutToBeInserted); + connect(filterModel, &QAbstractItemModel::rowsInserted, this, &VersionProxyModel::sourceRowsInserted); + connect(filterModel, &QAbstractItemModel::rowsAboutToBeRemoved, this, &VersionProxyModel::sourceRowsAboutToBeRemoved); + connect(filterModel, &QAbstractItemModel::rowsRemoved, this, &VersionProxyModel::sourceRowsRemoved); + // FIXME: implement when needed + /* + connect(replacing, &QAbstractItemModel::rowsAboutToBeMoved, this, &VersionProxyModel::sourceRowsAboutToBeMoved); + connect(replacing, &QAbstractItemModel::rowsMoved, this, &VersionProxyModel::sourceRowsMoved); + connect(replacing, &QAbstractItemModel::layoutAboutToBeChanged, this, &VersionProxyModel::sourceLayoutAboutToBeChanged); + connect(replacing, &QAbstractItemModel::layoutChanged, this, &VersionProxyModel::sourceLayoutChanged); + */ + connect(filterModel, &QAbstractItemModel::modelAboutToBeReset, this, &VersionProxyModel::sourceAboutToBeReset); + connect(filterModel, &QAbstractItemModel::modelReset, this, &VersionProxyModel::sourceReset); + + QAbstractProxyModel::setSourceModel(filterModel); +} + +QVariant VersionProxyModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(section < 0 || section >= m_columns.size()) + return QVariant(); + if(orientation != Qt::Horizontal) + return QVariant(); + auto column = m_columns[section]; + if(role == Qt::DisplayRole) + { + switch(column) + { + case Name: + return tr("Version"); + case ParentVersion: + return tr("Minecraft"); //FIXME: this should come from metadata + case Branch: + return tr("Branch"); + case Type: + return tr("Type"); + case Architecture: + return tr("Architecture"); + case Path: + return tr("Path"); + case Time: + return tr("Released"); + } + } + else if(role == Qt::ToolTipRole) + { + switch(column) + { + case Name: + return tr("The name of the version."); + case ParentVersion: + return tr("Minecraft version"); //FIXME: this should come from metadata + case Branch: + return tr("The version's branch"); + case Type: + return tr("The version's type"); + case Architecture: + return tr("CPU Architecture"); + case Path: + return tr("Filesystem path to this version"); + case Time: + return tr("Release date of this version"); + } + } + return QVariant(); +} + +QVariant VersionProxyModel::data(const QModelIndex &index, int role) const +{ + if(!index.isValid()) + { + return QVariant(); + } + auto column = m_columns[index.column()]; + auto parentIndex = mapToSource(index); + switch(role) + { + case Qt::DisplayRole: + { + switch(column) + { + case Name: + { + QString version = sourceModel()->data(parentIndex, BaseVersionList::VersionRole).toString(); + if(version == m_currentVersion) + { + return tr("%1 (installed)").arg(version); + } + return version; + } + case ParentVersion: + return sourceModel()->data(parentIndex, BaseVersionList::ParentVersionRole); + case Branch: + return sourceModel()->data(parentIndex, BaseVersionList::BranchRole); + case Type: + return sourceModel()->data(parentIndex, BaseVersionList::TypeRole); + case Architecture: + return sourceModel()->data(parentIndex, BaseVersionList::ArchitectureRole); + case Path: + return sourceModel()->data(parentIndex, BaseVersionList::PathRole); + case Time: + return sourceModel()->data(parentIndex, Meta::VersionList::TimeRole).toDate(); + default: + return QVariant(); + } + } + case Qt::ToolTipRole: + { + switch(column) + { + case Name: + { + if(hasRecommended) + { + auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); + if(value.toBool()) + { + return tr("Recommended"); + } + else if(hasLatest) + { + auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); + if(value.toBool()) + { + return tr("Latest"); + } + } + else if(index.row() == 0) + { + return tr("Latest"); + } + } + } + default: + { + return sourceModel()->data(parentIndex, BaseVersionList::VersionIdRole); + } + } + } + case Qt::DecorationRole: + { + switch(column) + { + case Name: + { + if(hasRecommended) + { + auto value = sourceModel()->data(parentIndex, BaseVersionList::RecommendedRole); + if(value.toBool()) + { + return APPLICATION->getThemedIcon("star"); + } + else if(hasLatest) + { + auto value = sourceModel()->data(parentIndex, BaseVersionList::LatestRole); + if(value.toBool()) + { + return APPLICATION->getThemedIcon("bug"); + } + } + else if(index.row() == 0) + { + return APPLICATION->getThemedIcon("bug"); + } + auto pixmap = QPixmapCache::find("placeholder"); + if(!pixmap) + { + QPixmap px(16,16); + px.fill(Qt::transparent); + QPixmapCache::insert("placeholder", px); + return px; + } + return *pixmap; + } + } + default: + { + return QVariant(); + } + } + } + default: + { + if(roles.contains((BaseVersionList::ModelRoles)role)) + { + return sourceModel()->data(parentIndex, role); + } + return QVariant(); + } + } +} + +QModelIndex VersionProxyModel::parent(const QModelIndex &child) const +{ + return QModelIndex(); +} + +QModelIndex VersionProxyModel::mapFromSource(const QModelIndex &sourceIndex) const +{ + if(sourceIndex.isValid()) + { + return index(sourceIndex.row(), 0); + } + return QModelIndex(); +} + +QModelIndex VersionProxyModel::mapToSource(const QModelIndex &proxyIndex) const +{ + if(proxyIndex.isValid()) + { + return sourceModel()->index(proxyIndex.row(), 0); + } + return QModelIndex(); +} + +QModelIndex VersionProxyModel::index(int row, int column, const QModelIndex &parent) const +{ + // no trees here... shoo + if(parent.isValid()) + { + return QModelIndex(); + } + if(row < 0 || row >= sourceModel()->rowCount()) + return QModelIndex(); + if(column < 0 || column >= columnCount()) + return QModelIndex(); + return QAbstractItemModel::createIndex(row, column); +} + +int VersionProxyModel::columnCount(const QModelIndex &parent) const +{ + return m_columns.size(); +} + +int VersionProxyModel::rowCount(const QModelIndex &parent) const +{ + if(sourceModel()) + { + return sourceModel()->rowCount(); + } + return 0; +} + +void VersionProxyModel::sourceDataChanged(const QModelIndex &source_top_left, + const QModelIndex &source_bottom_right) +{ + if(source_top_left.parent() != source_bottom_right.parent()) + return; + + // whole row is getting changed + auto topLeft = createIndex(source_top_left.row(), 0); + auto bottomRight = createIndex(source_bottom_right.row(), columnCount() - 1); + emit dataChanged(topLeft, bottomRight); +} + +void VersionProxyModel::setSourceModel(QAbstractItemModel *replacingRaw) +{ + auto replacing = dynamic_cast(replacingRaw); + beginResetModel(); + + m_columns.clear(); + if(!replacing) + { + roles.clear(); + filterModel->setSourceModel(replacing); + return; + } + + roles = replacing->providesRoles(); + if(roles.contains(BaseVersionList::VersionRole)) + { + m_columns.push_back(Name); + } + /* + if(roles.contains(BaseVersionList::ParentVersionRole)) + { + m_columns.push_back(ParentVersion); + } + */ + if(roles.contains(BaseVersionList::ArchitectureRole)) + { + m_columns.push_back(Architecture); + } + if(roles.contains(BaseVersionList::PathRole)) + { + m_columns.push_back(Path); + } + if(roles.contains(Meta::VersionList::TimeRole)) + { + m_columns.push_back(Time); + } + if(roles.contains(BaseVersionList::BranchRole)) + { + m_columns.push_back(Branch); + } + if(roles.contains(BaseVersionList::TypeRole)) + { + m_columns.push_back(Type); + } + if(roles.contains(BaseVersionList::RecommendedRole)) + { + hasRecommended = true; + } + if(roles.contains(BaseVersionList::LatestRole)) + { + hasLatest = true; + } + filterModel->setSourceModel(replacing); + + endResetModel(); +} + +QModelIndex VersionProxyModel::getRecommended() const +{ + if(!roles.contains(BaseVersionList::RecommendedRole)) + { + return index(0, 0); + } + int recommended = 0; + for (int i = 0; i < rowCount(); i++) + { + auto value = sourceModel()->data(mapToSource(index(i, 0)), BaseVersionList::RecommendedRole); + if (value.toBool()) + { + recommended = i; + } + } + return index(recommended, 0); +} + +QModelIndex VersionProxyModel::getVersion(const QString& version) const +{ + int found = -1; + for (int i = 0; i < rowCount(); i++) + { + auto value = sourceModel()->data(mapToSource(index(i, 0)), BaseVersionList::VersionRole); + if (value.toString() == version) + { + found = i; + } + } + if(found == -1) + { + return QModelIndex(); + } + return index(found, 0); +} + +void VersionProxyModel::clearFilters() +{ + m_filters.clear(); + filterModel->filterChanged(); +} + +void VersionProxyModel::setFilter(const BaseVersionList::ModelRoles column, Filter * f) +{ + m_filters[column].reset(f); + filterModel->filterChanged(); +} + +const VersionProxyModel::FilterMap &VersionProxyModel::filters() const +{ + return m_filters; +} + +void VersionProxyModel::sourceAboutToBeReset() +{ + beginResetModel(); +} + +void VersionProxyModel::sourceReset() +{ + endResetModel(); +} + +void VersionProxyModel::sourceRowsAboutToBeInserted(const QModelIndex& parent, int first, int last) +{ + beginInsertRows(parent, first, last); +} + +void VersionProxyModel::sourceRowsInserted(const QModelIndex& parent, int first, int last) +{ + endInsertRows(); +} + +void VersionProxyModel::sourceRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) +{ + beginRemoveRows(parent, first, last); +} + +void VersionProxyModel::sourceRowsRemoved(const QModelIndex& parent, int first, int last) +{ + endRemoveRows(); +} + +void VersionProxyModel::setCurrentVersion(const QString &version) +{ + m_currentVersion = version; +} + +#include "VersionProxyModel.moc" diff --git a/ultimmc/launcher/VersionProxyModel.h b/ultimmc/launcher/VersionProxyModel.h new file mode 100644 index 0000000..8991c31 --- /dev/null +++ b/ultimmc/launcher/VersionProxyModel.h @@ -0,0 +1,67 @@ +#pragma once +#include +#include "BaseVersionList.h" + +#include + +class VersionFilterModel; + +class VersionProxyModel: public QAbstractProxyModel +{ + Q_OBJECT +public: + + enum Column + { + Name, + ParentVersion, + Branch, + Type, + Architecture, + Path, + Time + }; + typedef QHash> FilterMap; + +public: + VersionProxyModel ( QObject* parent = 0 ); + virtual ~VersionProxyModel() {}; + + virtual int columnCount(const QModelIndex &parent = QModelIndex()) const override; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + virtual QModelIndex mapFromSource(const QModelIndex &sourceIndex) const override; + virtual QModelIndex mapToSource(const QModelIndex &proxyIndex) const override; + virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + virtual QModelIndex parent(const QModelIndex &child) const override; + virtual void setSourceModel(QAbstractItemModel *sourceModel) override; + + const FilterMap &filters() const; + void setFilter(const BaseVersionList::ModelRoles column, Filter * filter); + void clearFilters(); + QModelIndex getRecommended() const; + QModelIndex getVersion(const QString & version) const; + void setCurrentVersion(const QString &version); +private slots: + + void sourceDataChanged(const QModelIndex &source_top_left,const QModelIndex &source_bottom_right); + + void sourceAboutToBeReset(); + void sourceReset(); + + void sourceRowsAboutToBeInserted(const QModelIndex &parent, int first, int last); + void sourceRowsInserted(const QModelIndex &parent, int first, int last); + + void sourceRowsAboutToBeRemoved(const QModelIndex &parent, int first, int last); + void sourceRowsRemoved(const QModelIndex &parent, int first, int last); + +private: + QList m_columns; + FilterMap m_filters; + BaseVersionList::RoleList roles; + VersionFilterModel * filterModel; + bool hasRecommended = false; + bool hasLatest = false; + QString m_currentVersion; +}; diff --git a/ultimmc/launcher/Version_test.cpp b/ultimmc/launcher/Version_test.cpp new file mode 100644 index 0000000..b2d657a --- /dev/null +++ b/ultimmc/launcher/Version_test.cpp @@ -0,0 +1,85 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "TestUtil.h" +#include + +class ModUtilsTest : public QObject +{ + Q_OBJECT + void setupVersions() + { + QTest::addColumn("first"); + QTest::addColumn("second"); + QTest::addColumn("lessThan"); + QTest::addColumn("equal"); + + QTest::newRow("equal, explicit") << "1.2.0" << "1.2.0" << false << true; + QTest::newRow("equal, implicit 1") << "1.2" << "1.2.0" << false << true; + QTest::newRow("equal, implicit 2") << "1.2.0" << "1.2" << false << true; + QTest::newRow("equal, two-digit") << "1.42" << "1.42" << false << true; + + QTest::newRow("lessThan, explicit 1") << "1.2.0" << "1.2.1" << true << false; + QTest::newRow("lessThan, explicit 2") << "1.2.0" << "1.3.0" << true << false; + QTest::newRow("lessThan, explicit 3") << "1.2.0" << "2.2.0" << true << false; + QTest::newRow("lessThan, implicit 1") << "1.2" << "1.2.1" << true << false; + QTest::newRow("lessThan, implicit 2") << "1.2" << "1.3.0" << true << false; + QTest::newRow("lessThan, implicit 3") << "1.2" << "2.2.0" << true << false; + QTest::newRow("lessThan, two-digit") << "1.41" << "1.42" << true << false; + + QTest::newRow("greaterThan, explicit 1") << "1.2.1" << "1.2.0" << false << false; + QTest::newRow("greaterThan, explicit 2") << "1.3.0" << "1.2.0" << false << false; + QTest::newRow("greaterThan, explicit 3") << "2.2.0" << "1.2.0" << false << false; + QTest::newRow("greaterThan, implicit 1") << "1.2.1" << "1.2" << false << false; + QTest::newRow("greaterThan, implicit 2") << "1.3.0" << "1.2" << false << false; + QTest::newRow("greaterThan, implicit 3") << "2.2.0" << "1.2" << false << false; + QTest::newRow("greaterThan, two-digit") << "1.42" << "1.41" << false << false; + } + +private slots: + void initTestCase() + { + + } + void cleanupTestCase() + { + + } + + void test_versionCompare_data() + { + setupVersions(); + } + void test_versionCompare() + { + QFETCH(QString, first); + QFETCH(QString, second); + QFETCH(bool, lessThan); + QFETCH(bool, equal); + + const auto v1 = Version(first); + const auto v2 = Version(second); + + QCOMPARE(v1 < v2, lessThan); + QCOMPARE(v1 > v2, !lessThan && !equal); + QCOMPARE(v1 == v2, equal); + } +}; + +QTEST_GUILESS_MAIN(ModUtilsTest) + +#include "Version_test.moc" diff --git a/ultimmc/launcher/WatchLock.h b/ultimmc/launcher/WatchLock.h new file mode 100644 index 0000000..3e08b41 --- /dev/null +++ b/ultimmc/launcher/WatchLock.h @@ -0,0 +1,20 @@ + +#pragma once + +#include +#include + +struct WatchLock +{ + WatchLock(QFileSystemWatcher * watcher, const QString& directory) + : m_watcher(watcher), m_directory(directory) + { + m_watcher->removePath(m_directory); + } + ~WatchLock() + { + m_watcher->addPath(m_directory); + } + QFileSystemWatcher * m_watcher; + QString m_directory; +}; diff --git a/ultimmc/launcher/icons/IconList.cpp b/ultimmc/launcher/icons/IconList.cpp new file mode 100644 index 0000000..584edd6 --- /dev/null +++ b/ultimmc/launcher/icons/IconList.cpp @@ -0,0 +1,419 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "IconList.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#define MAX_SIZE 1024 + +IconList::IconList(const QStringList &builtinPaths, QString path, QObject *parent) : QAbstractListModel(parent) +{ + QSet builtinNames; + + // add builtin icons + for(auto & builtinPath: builtinPaths) + { + QDir instance_icons(builtinPath); + auto file_info_list = instance_icons.entryInfoList(QDir::Files, QDir::Name); + for (auto file_info : file_info_list) + { + builtinNames.insert(file_info.baseName()); + } + } + for(auto & builtinName : builtinNames) + { + addThemeIcon(builtinName); + } + + m_watcher.reset(new QFileSystemWatcher()); + is_watching = false; + connect(m_watcher.get(), SIGNAL(directoryChanged(QString)), + SLOT(directoryChanged(QString))); + connect(m_watcher.get(), SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString))); + + directoryChanged(path); +} + +void IconList::directoryChanged(const QString &path) +{ + QDir new_dir (path); + if(m_dir.absolutePath() != new_dir.absolutePath()) + { + m_dir.setPath(path); + m_dir.refresh(); + if(is_watching) + stopWatching(); + startWatching(); + } + if(!m_dir.exists()) + if(!FS::ensureFolderPathExists(m_dir.absolutePath())) + return; + m_dir.refresh(); + auto new_list = m_dir.entryList(QDir::Files, QDir::Name); + for (auto it = new_list.begin(); it != new_list.end(); it++) + { + QString &foo = (*it); + foo = m_dir.filePath(foo); + } + auto new_set = new_list.toSet(); + QList current_list; + for (auto &it : icons) + { + if (!it.has(IconType::FileBased)) + continue; + current_list.push_back(it.m_images[IconType::FileBased].filename); + } + QSet current_set = current_list.toSet(); + + QSet to_remove = current_set; + to_remove -= new_set; + + QSet to_add = new_set; + to_add -= current_set; + + for (auto remove : to_remove) + { + qDebug() << "Removing " << remove; + QFileInfo rmfile(remove); + QString key = rmfile.baseName(); + int idx = getIconIndex(key); + if (idx == -1) + continue; + icons[idx].remove(IconType::FileBased); + if (icons[idx].type() == IconType::ToBeDeleted) + { + beginRemoveRows(QModelIndex(), idx, idx); + icons.remove(idx); + reindex(); + endRemoveRows(); + } + else + { + dataChanged(index(idx), index(idx)); + } + m_watcher->removePath(remove); + emit iconUpdated(key); + } + + for (auto add : to_add) + { + qDebug() << "Adding " << add; + QFileInfo addfile(add); + QString key = addfile.baseName(); + if (addIcon(key, QString(), addfile.filePath(), IconType::FileBased)) + { + m_watcher->addPath(add); + emit iconUpdated(key); + } + } +} + +void IconList::fileChanged(const QString &path) +{ + qDebug() << "Checking " << path; + QFileInfo checkfile(path); + if (!checkfile.exists()) + return; + QString key = checkfile.baseName(); + int idx = getIconIndex(key); + if (idx == -1) + return; + QIcon icon(path); + if (!icon.availableSizes().size()) + return; + + icons[idx].m_images[IconType::FileBased].icon = icon; + dataChanged(index(idx), index(idx)); + emit iconUpdated(key); +} + +void IconList::SettingChanged(const Setting &setting, QVariant value) +{ + if(setting.id() != "IconsDir") + return; + + directoryChanged(value.toString()); +} + +void IconList::startWatching() +{ + auto abs_path = m_dir.absolutePath(); + FS::ensureFolderPathExists(abs_path); + is_watching = m_watcher->addPath(abs_path); + if (is_watching) + { + qDebug() << "Started watching " << abs_path; + } + else + { + qDebug() << "Failed to start watching " << abs_path; + } +} + +void IconList::stopWatching() +{ + m_watcher->removePaths(m_watcher->files()); + m_watcher->removePaths(m_watcher->directories()); + is_watching = false; +} + +QStringList IconList::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} +Qt::DropActions IconList::supportedDropActions() const +{ + return Qt::CopyAction; +} + +bool IconList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + + // files dropped from outside? + if (data->hasUrls()) + { + auto urls = data->urls(); + QStringList iconFiles; + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + iconFiles += url.toLocalFile(); + } + installIcons(iconFiles); + return true; + } + return false; +} + +Qt::ItemFlags IconList::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + if (index.isValid()) + return Qt::ItemIsDropEnabled | defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +QVariant IconList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + + if (row < 0 || row >= icons.size()) + return QVariant(); + + switch (role) + { + case Qt::DecorationRole: + return icons[row].icon(); + case Qt::DisplayRole: + return icons[row].name(); + case Qt::UserRole: + return icons[row].m_key; + default: + return QVariant(); + } +} + +int IconList::rowCount(const QModelIndex &parent) const +{ + return icons.size(); +} + +void IconList::installIcons(const QStringList &iconFiles) +{ + for (QString file : iconFiles) + { + QFileInfo fileinfo(file); + if (!fileinfo.isReadable() || !fileinfo.isFile()) + continue; + QString target = FS::PathCombine(m_dir.dirName(), fileinfo.fileName()); + + QString suffix = fileinfo.suffix(); + if (suffix != "jpeg" && suffix != "png" && suffix != "jpg" && suffix != "ico" && suffix != "svg" && suffix != "gif") + continue; + + if (!QFile::copy(file, target)) + continue; + } +} + +void IconList::installIcon(const QString &file, const QString &name) +{ + QFileInfo fileinfo(file); + if(!fileinfo.isReadable() || !fileinfo.isFile()) + return; + + QString target = FS::PathCombine(m_dir.dirName(), name); + + QFile::copy(file, target); +} + +bool IconList::iconFileExists(const QString &key) const +{ + auto iconEntry = icon(key); + if(!iconEntry) + { + return false; + } + return iconEntry->has(IconType::FileBased); +} + +const MMCIcon *IconList::icon(const QString &key) const +{ + int iconIdx = getIconIndex(key); + if (iconIdx == -1) + return nullptr; + return &icons[iconIdx]; +} + +bool IconList::deleteIcon(const QString &key) +{ + int iconIdx = getIconIndex(key); + if (iconIdx == -1) + return false; + auto &iconEntry = icons[iconIdx]; + if (iconEntry.has(IconType::FileBased)) + { + return QFile::remove(iconEntry.m_images[IconType::FileBased].filename); + } + return false; +} + +bool IconList::addThemeIcon(const QString& key) +{ + auto iter = name_index.find(key); + if (iter != name_index.end()) + { + auto &oldOne = icons[*iter]; + oldOne.replace(Builtin, key); + dataChanged(index(*iter), index(*iter)); + return true; + } + else + { + // add a new icon + beginInsertRows(QModelIndex(), icons.size(), icons.size()); + { + MMCIcon mmc_icon; + mmc_icon.m_name = key; + mmc_icon.m_key = key; + mmc_icon.replace(Builtin, key); + icons.push_back(mmc_icon); + name_index[key] = icons.size() - 1; + } + endInsertRows(); + return true; + } +} + +bool IconList::addIcon(const QString &key, const QString &name, const QString &path, const IconType type) +{ + // replace the icon even? is the input valid? + QIcon icon(path); + if (icon.isNull()) + return false; + auto iter = name_index.find(key); + if (iter != name_index.end()) + { + auto &oldOne = icons[*iter]; + oldOne.replace(type, icon, path); + dataChanged(index(*iter), index(*iter)); + return true; + } + else + { + // add a new icon + beginInsertRows(QModelIndex(), icons.size(), icons.size()); + { + MMCIcon mmc_icon; + mmc_icon.m_name = name; + mmc_icon.m_key = key; + mmc_icon.replace(type, icon, path); + icons.push_back(mmc_icon); + name_index[key] = icons.size() - 1; + } + endInsertRows(); + return true; + } +} + +void IconList::saveIcon(const QString &key, const QString &path, const char * format) const +{ + auto icon = getIcon(key); + auto pixmap = icon.pixmap(128, 128); + pixmap.save(path, format); +} + + +void IconList::reindex() +{ + name_index.clear(); + int i = 0; + for (auto &iter : icons) + { + name_index[iter.m_key] = i; + i++; + } +} + +QIcon IconList::getIcon(const QString &key) const +{ + int icon_index = getIconIndex(key); + + if (icon_index != -1) + return icons[icon_index].icon(); + + // Fallback for icons that don't exist. + icon_index = getIconIndex("grass"); + + if (icon_index != -1) + return icons[icon_index].icon(); + return QIcon(); +} + +int IconList::getIconIndex(const QString &key) const +{ + auto iter = name_index.find(key == "default" ? "grass" : key); + if (iter != name_index.end()) + return *iter; + + return -1; +} + +QString IconList::getDirectory() const +{ + return m_dir.absolutePath(); +} + +//#include "IconList.moc" diff --git a/ultimmc/launcher/icons/IconList.h b/ultimmc/launcher/icons/IconList.h new file mode 100644 index 0000000..ebbb52e --- /dev/null +++ b/ultimmc/launcher/icons/IconList.h @@ -0,0 +1,87 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "MMCIcon.h" +#include "settings/Setting.h" + +#include "QObjectPtr.h" + +class QFileSystemWatcher; + +class IconList : public QAbstractListModel +{ + Q_OBJECT +public: + explicit IconList(const QStringList &builtinPaths, QString path, QObject *parent = 0); + virtual ~IconList() {}; + + QIcon getIcon(const QString &key) const; + int getIconIndex(const QString &key) const; + QString getDirectory() const; + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + virtual QStringList mimeTypes() const override; + virtual Qt::DropActions supportedDropActions() const override; + virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override; + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + + bool addThemeIcon(const QString &key); + bool addIcon(const QString &key, const QString &name, const QString &path, const IconType type); + void saveIcon(const QString &key, const QString &path, const char * format) const; + bool deleteIcon(const QString &key); + bool iconFileExists(const QString &key) const; + + void installIcons(const QStringList &iconFiles); + void installIcon(const QString &file, const QString &name); + + const MMCIcon * icon(const QString &key) const; + + void startWatching(); + void stopWatching(); + +signals: + void iconUpdated(QString key); + +private: + // hide copy constructor + IconList(const IconList &) = delete; + // hide assign op + IconList &operator=(const IconList &) = delete; + void reindex(); + +public slots: + void directoryChanged(const QString &path); + +protected slots: + void fileChanged(const QString &path); + void SettingChanged(const Setting & setting, QVariant value); +private: + shared_qobject_ptr m_watcher; + bool is_watching; + QMap name_index; + QVector icons; + QDir m_dir; +}; diff --git a/ultimmc/launcher/icons/IconUtils.cpp b/ultimmc/launcher/icons/IconUtils.cpp new file mode 100644 index 0000000..bf530c1 --- /dev/null +++ b/ultimmc/launcher/icons/IconUtils.cpp @@ -0,0 +1,62 @@ +#include "IconUtils.h" + +#include "FileSystem.h" +#include + +#include + +namespace { +std::array validIconExtensions = {{ + "svg", + "png", + "ico", + "gif", + "jpg", + "jpeg" +}}; +} + +namespace IconUtils{ + +QString findBestIconIn(const QString &folder, const QString & iconKey) { + int best_found = validIconExtensions.size(); + QString best_filename; + + QDirIterator it(folder, QDir::NoDotAndDotDot | QDir::Files, QDirIterator::NoIteratorFlags); + while (it.hasNext()) { + it.next(); + auto fileInfo = it.fileInfo(); + + if(fileInfo.completeBaseName() != iconKey) + continue; + + auto extension = fileInfo.suffix(); + + for(int i = 0; i < best_found; i++) { + if(extension == validIconExtensions[i]) { + best_found = i; + qDebug() << i << " : " << fileInfo.fileName(); + best_filename = fileInfo.fileName(); + } + } + } + return FS::PathCombine(folder, best_filename); +} + +QString getIconFilter() { + QString out; + QTextStream stream(&out); + stream << '('; + for(size_t i = 0; i < validIconExtensions.size() - 1; i++) { + if(i > 0) { + stream << " "; + } + stream << "*." << validIconExtensions[i]; + } + stream << " *." << validIconExtensions[validIconExtensions.size() - 1]; + stream << ')'; + return out; +} + +} + diff --git a/ultimmc/launcher/icons/IconUtils.h b/ultimmc/launcher/icons/IconUtils.h new file mode 100644 index 0000000..be93d91 --- /dev/null +++ b/ultimmc/launcher/icons/IconUtils.h @@ -0,0 +1,13 @@ +#pragma once + +#include + +namespace IconUtils { + +// Given a folder and an icon key, find 'best' of the icons with the given key in there and return its path +QString findBestIconIn(const QString &folder, const QString & iconKey); + +// Get icon file type filter for file browser dialogs +QString getIconFilter(); + +} diff --git a/ultimmc/launcher/icons/MMCIcon.cpp b/ultimmc/launcher/icons/MMCIcon.cpp new file mode 100644 index 0000000..f0b82ec --- /dev/null +++ b/ultimmc/launcher/icons/MMCIcon.cpp @@ -0,0 +1,118 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MMCIcon.h" +#include +#include + +IconType operator--(IconType &t, int) +{ + IconType temp = t; + switch (t) + { + case IconType::Builtin: + t = IconType::ToBeDeleted; + break; + case IconType::Transient: + t = IconType::Builtin; + break; + case IconType::FileBased: + t = IconType::Transient; + break; + default: + { + } + } + return temp; +} + +IconType MMCIcon::type() const +{ + return m_current_type; +} + +QString MMCIcon::name() const +{ + if (m_name.size()) + return m_name; + return m_key; +} + +bool MMCIcon::has(IconType _type) const +{ + return m_images[_type].present(); +} + +QIcon MMCIcon::icon() const +{ + if (m_current_type == IconType::ToBeDeleted) + return QIcon(); + auto & icon = m_images[m_current_type].icon; + if(!icon.isNull()) + return icon; + // FIXME: inject this. + return XdgIcon::fromTheme(m_images[m_current_type].key); +} + +void MMCIcon::remove(IconType rm_type) +{ + m_images[rm_type].filename = QString(); + m_images[rm_type].icon = QIcon(); + for (auto iter = rm_type; iter != IconType::ToBeDeleted; iter--) + { + if (m_images[iter].present()) + { + m_current_type = iter; + return; + } + } + m_current_type = IconType::ToBeDeleted; +} + +void MMCIcon::replace(IconType new_type, QIcon icon, QString path) +{ + if (new_type > m_current_type || m_current_type == IconType::ToBeDeleted) + { + m_current_type = new_type; + } + m_images[new_type].icon = icon; + m_images[new_type].filename = path; + m_images[new_type].key = QString(); +} + +void MMCIcon::replace(IconType new_type, const QString& key) +{ + if (new_type > m_current_type || m_current_type == IconType::ToBeDeleted) + { + m_current_type = new_type; + } + m_images[new_type].icon = QIcon(); + m_images[new_type].filename = QString(); + m_images[new_type].key = key; +} + +QString MMCIcon::getFilePath() const +{ + if(m_current_type == IconType::ToBeDeleted){ + return QString(); + } + return m_images[m_current_type].filename; +} + + +bool MMCIcon::isBuiltIn() const +{ + return m_current_type == IconType::Builtin; +} diff --git a/ultimmc/launcher/icons/MMCIcon.h b/ultimmc/launcher/icons/MMCIcon.h new file mode 100644 index 0000000..13d9931 --- /dev/null +++ b/ultimmc/launcher/icons/MMCIcon.h @@ -0,0 +1,57 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include + +enum IconType : unsigned +{ + Builtin, + Transient, + FileBased, + ICONS_TOTAL, + ToBeDeleted +}; + +struct MMCImage +{ + QIcon icon; + QString key; + QString filename; + bool present() const + { + return !icon.isNull() || !key.isEmpty(); + } +}; + +struct MMCIcon +{ + QString m_key; + QString m_name; + MMCImage m_images[ICONS_TOTAL]; + IconType m_current_type = ToBeDeleted; + + IconType type() const; + QString name() const; + bool has(IconType _type) const; + QIcon icon() const; + void remove(IconType rm_type); + void replace(IconType new_type, QIcon icon, QString path = QString()); + void replace(IconType new_type, const QString &key); + bool isBuiltIn() const; + QString getFilePath() const; +}; diff --git a/ultimmc/launcher/install_prereqs.cmake.in b/ultimmc/launcher/install_prereqs.cmake.in new file mode 100644 index 0000000..e4408d1 --- /dev/null +++ b/ultimmc/launcher/install_prereqs.cmake.in @@ -0,0 +1,27 @@ +set(CMAKE_MODULE_PATH "@CMAKE_MODULE_PATH@") + +file(GLOB_RECURSE QTPLUGINS "${CMAKE_INSTALL_PREFIX}/@PLUGIN_DEST_DIR@/*@CMAKE_SHARED_LIBRARY_SUFFIX@") +function(gp_resolved_file_type_override resolved_file type_var) + if(resolved_file MATCHES "^/(usr/)?lib/libQt") + set(${type_var} other PARENT_SCOPE) + elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libxcb-") + set(${type_var} other PARENT_SCOPE) + elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libicu") + set(${type_var} other PARENT_SCOPE) + elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libpng") + set(${type_var} other PARENT_SCOPE) + elseif(resolved_file MATCHES "^/(usr/)?lib(.+)?/libproxy") + set(${type_var} other PARENT_SCOPE) + elseif((resolved_file MATCHES "^/(usr/)?lib(.+)?/libstdc\\+\\+") AND (UNIX AND NOT APPLE)) + set(${type_var} other PARENT_SCOPE) + endif() +endfunction() + +set(gp_tool "@CMAKE_GP_TOOL@") +set(gp_cmd_paths ${gp_cmd_paths} + "@CMAKE_GP_CMD_PATHS@" +) + +include(BundleUtilities) +fixup_bundle("@APPS@" "${QTPLUGINS}" "@DIRS@") + diff --git a/ultimmc/launcher/java/JavaChecker.cpp b/ultimmc/launcher/java/JavaChecker.cpp new file mode 100644 index 0000000..35ddc35 --- /dev/null +++ b/ultimmc/launcher/java/JavaChecker.cpp @@ -0,0 +1,174 @@ +#include "JavaChecker.h" + +#include +#include +#include +#include + +#include "JavaUtils.h" +#include "FileSystem.h" +#include "Commandline.h" +#include "Application.h" + +JavaChecker::JavaChecker(QObject *parent) : QObject(parent) +{ +} + +void JavaChecker::performCheck() +{ + QString checkerJar = FS::PathCombine(APPLICATION->getJarsPath(), "JavaCheck.jar"); + + QStringList args; + + process.reset(new QProcess()); + if(m_args.size()) + { + auto extraArgs = Commandline::splitArgs(m_args); + args.append(extraArgs); + } + if(m_minMem != 0) + { + args << QString("-Xms%1m").arg(m_minMem); + } + if(m_maxMem != 0) + { + args << QString("-Xmx%1m").arg(m_maxMem); + } + if(m_permGen != 64) + { + args << QString("-XX:PermSize=%1m").arg(m_permGen); + } + + args.append({"-jar", checkerJar}); + process->setArguments(args); + process->setProgram(m_path); + process->setProcessChannelMode(QProcess::SeparateChannels); + process->setProcessEnvironment(CleanEnviroment()); + qDebug() << "Running java checker: " + m_path + args.join(" ");; + + connect(process.get(), SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(finished(int, QProcess::ExitStatus))); + connect(process.get(), SIGNAL(error(QProcess::ProcessError)), this, SLOT(error(QProcess::ProcessError))); + connect(process.get(), SIGNAL(readyReadStandardOutput()), this, SLOT(stdoutReady())); + connect(process.get(), SIGNAL(readyReadStandardError()), this, SLOT(stderrReady())); + connect(&killTimer, SIGNAL(timeout()), SLOT(timeout())); + killTimer.setSingleShot(true); + killTimer.start(15000); + process->start(); +} + +void JavaChecker::stdoutReady() +{ + QByteArray data = process->readAllStandardOutput(); + QString added = QString::fromLocal8Bit(data); + added.remove('\r'); + m_stdout += added; +} + +void JavaChecker::stderrReady() +{ + QByteArray data = process->readAllStandardError(); + QString added = QString::fromLocal8Bit(data); + added.remove('\r'); + m_stderr += added; +} + +void JavaChecker::finished(int exitcode, QProcess::ExitStatus status) +{ + killTimer.stop(); + QProcessPtr _process = process; + process.reset(); + + JavaCheckResult result; + { + result.path = m_path; + result.id = m_id; + } + result.errorLog = m_stderr; + result.outLog = m_stdout; + qDebug() << "STDOUT" << m_stdout; + qWarning() << "STDERR" << m_stderr; + qDebug() << "Java checker finished with status " << status << " exit code " << exitcode; + + if (status == QProcess::CrashExit || exitcode == 1) + { + result.validity = JavaCheckResult::Validity::Errored; + emit checkFinished(result); + return; + } + + bool success = true; + + QMap results; + QStringList lines = m_stdout.split("\n", QString::SkipEmptyParts); + for(QString line : lines) + { + line = line.trimmed(); + // NOTE: workaround for GH-4125, where garbage is getting printed into stdout on bedrock linux + if (line.contains("/bedrock/strata")) { + continue; + } + + auto parts = line.split('=', QString::SkipEmptyParts); + if(parts.size() != 2 || parts[0].isEmpty() || parts[1].isEmpty()) + { + continue; + } + else + { + results.insert(parts[0], parts[1]); + } + } + + if(!results.contains("os.arch") || !results.contains("java.version") || !results.contains("java.vendor") || !success) + { + result.validity = JavaCheckResult::Validity::ReturnedInvalidData; + emit checkFinished(result); + return; + } + + auto os_arch = results["os.arch"]; + auto java_version = results["java.version"]; + auto java_vendor = results["java.vendor"]; + bool is_64 = os_arch == "x86_64" || os_arch == "amd64"; + + + result.validity = JavaCheckResult::Validity::Valid; + result.is_64bit = is_64; + result.mojangPlatform = is_64 ? "64" : "32"; + result.realPlatform = os_arch; + result.javaVersion = java_version; + result.javaVendor = java_vendor; + qDebug() << "Java checker succeeded."; + emit checkFinished(result); +} + +void JavaChecker::error(QProcess::ProcessError err) +{ + if(err == QProcess::FailedToStart) + { + qDebug() << "Java checker has failed to start."; + qDebug() << "Process environment:"; + qDebug() << process->environment(); + qDebug() << "Native environment:"; + qDebug() << QProcessEnvironment::systemEnvironment().toStringList(); + killTimer.stop(); + JavaCheckResult result; + { + result.path = m_path; + result.id = m_id; + } + + emit checkFinished(result); + return; + } +} + +void JavaChecker::timeout() +{ + // NO MERCY. NO ABUSE. + if(process) + { + qDebug() << "Java checker has been killed by timeout."; + process->kill(); + } +} diff --git a/ultimmc/launcher/java/JavaChecker.h b/ultimmc/launcher/java/JavaChecker.h new file mode 100644 index 0000000..122861c --- /dev/null +++ b/ultimmc/launcher/java/JavaChecker.h @@ -0,0 +1,61 @@ +#pragma once +#include +#include +#include + +#include "QObjectPtr.h" + +#include "JavaVersion.h" + +class JavaChecker; + +struct JavaCheckResult +{ + QString path; + QString mojangPlatform; + QString realPlatform; + JavaVersion javaVersion; + QString javaVendor; + QString outLog; + QString errorLog; + bool is_64bit = false; + int id; + enum class Validity + { + Errored, + ReturnedInvalidData, + Valid + } validity = Validity::Errored; +}; + +typedef shared_qobject_ptr QProcessPtr; +typedef shared_qobject_ptr JavaCheckerPtr; +class JavaChecker : public QObject +{ + Q_OBJECT +public: + explicit JavaChecker(QObject *parent = 0); + void performCheck(); + + QString m_path; + QString m_args; + int m_id = 0; + int m_minMem = 0; + int m_maxMem = 0; + int m_permGen = 64; + +signals: + void checkFinished(JavaCheckResult result); +private: + QProcessPtr process; + QTimer killTimer; + QString m_stdout; + QString m_stderr; +public +slots: + void timeout(); + void finished(int exitcode, QProcess::ExitStatus); + void error(QProcess::ProcessError); + void stdoutReady(); + void stderrReady(); +}; diff --git a/ultimmc/launcher/java/JavaCheckerJob.cpp b/ultimmc/launcher/java/JavaCheckerJob.cpp new file mode 100644 index 0000000..67d7006 --- /dev/null +++ b/ultimmc/launcher/java/JavaCheckerJob.cpp @@ -0,0 +1,44 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "JavaCheckerJob.h" + +#include + +void JavaCheckerJob::partFinished(JavaCheckResult result) +{ + num_finished++; + qDebug() << m_job_name.toLocal8Bit() << "progress:" << num_finished << "/" + << javacheckers.size(); + setProgress(num_finished, javacheckers.size()); + + javaresults.replace(result.id, result); + + if (num_finished == javacheckers.size()) + { + emitSucceeded(); + } +} + +void JavaCheckerJob::executeTask() +{ + qDebug() << m_job_name.toLocal8Bit() << " started."; + for (auto iter : javacheckers) + { + javaresults.append(JavaCheckResult()); + connect(iter.get(), SIGNAL(checkFinished(JavaCheckResult)), SLOT(partFinished(JavaCheckResult))); + iter->performCheck(); + } +} diff --git a/ultimmc/launcher/java/JavaCheckerJob.h b/ultimmc/launcher/java/JavaCheckerJob.h new file mode 100644 index 0000000..c098642 --- /dev/null +++ b/ultimmc/launcher/java/JavaCheckerJob.h @@ -0,0 +1,61 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "JavaChecker.h" +#include "tasks/Task.h" + +class JavaCheckerJob; +typedef shared_qobject_ptr JavaCheckerJobPtr; + +// FIXME: this just seems horribly redundant +class JavaCheckerJob : public Task +{ + Q_OBJECT +public: + explicit JavaCheckerJob(QString job_name) : Task(), m_job_name(job_name) {}; + virtual ~JavaCheckerJob() {}; + + bool addJavaCheckerAction(JavaCheckerPtr base) + { + javacheckers.append(base); + // if this is already running, the action needs to be started right away! + if (isRunning()) + { + setProgress(num_finished, javacheckers.size()); + connect(base.get(), &JavaChecker::checkFinished, this, &JavaCheckerJob::partFinished); + base->performCheck(); + } + return true; + } + QList getResults() + { + return javaresults; + } + +private slots: + void partFinished(JavaCheckResult result); + +protected: + virtual void executeTask() override; + +private: + QString m_job_name; + QList javacheckers; + QList javaresults; + int num_finished = 0; +}; diff --git a/ultimmc/launcher/java/JavaInstall.cpp b/ultimmc/launcher/java/JavaInstall.cpp new file mode 100644 index 0000000..5bcf7bc --- /dev/null +++ b/ultimmc/launcher/java/JavaInstall.cpp @@ -0,0 +1,28 @@ +#include "JavaInstall.h" +#include + +bool JavaInstall::operator<(const JavaInstall &rhs) +{ + auto archCompare = Strings::naturalCompare(arch, rhs.arch, Qt::CaseInsensitive); + if(archCompare != 0) + return archCompare < 0; + if(id < rhs.id) + { + return true; + } + if(id > rhs.id) + { + return false; + } + return Strings::naturalCompare(path, rhs.path, Qt::CaseInsensitive) < 0; +} + +bool JavaInstall::operator==(const JavaInstall &rhs) +{ + return arch == rhs.arch && id == rhs.id && path == rhs.path; +} + +bool JavaInstall::operator>(const JavaInstall &rhs) +{ + return (!operator<(rhs)) && (!operator==(rhs)); +} diff --git a/ultimmc/launcher/java/JavaInstall.h b/ultimmc/launcher/java/JavaInstall.h new file mode 100644 index 0000000..64be40d --- /dev/null +++ b/ultimmc/launcher/java/JavaInstall.h @@ -0,0 +1,38 @@ +#pragma once + +#include "BaseVersion.h" +#include "JavaVersion.h" + +struct JavaInstall : public BaseVersion +{ + JavaInstall(){} + JavaInstall(QString id, QString arch, QString path) + : id(id), arch(arch), path(path) + { + } + virtual QString descriptor() + { + return id.toString(); + } + + virtual QString name() + { + return id.toString(); + } + + virtual QString typeString() const + { + return arch; + } + + bool operator<(const JavaInstall & rhs); + bool operator==(const JavaInstall & rhs); + bool operator>(const JavaInstall & rhs); + + JavaVersion id; + QString arch; + QString path; + bool recommended = false; +}; + +typedef std::shared_ptr JavaInstallPtr; diff --git a/ultimmc/launcher/java/JavaInstallList.cpp b/ultimmc/launcher/java/JavaInstallList.cpp new file mode 100644 index 0000000..07f2bd8 --- /dev/null +++ b/ultimmc/launcher/java/JavaInstallList.cpp @@ -0,0 +1,208 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include + +#include "java/JavaInstallList.h" +#include "java/JavaCheckerJob.h" +#include "java/JavaUtils.h" +#include "MMCStrings.h" +#include "minecraft/VersionFilterData.h" + +JavaInstallList::JavaInstallList(QObject *parent) : BaseVersionList(parent) +{ +} + +Task::Ptr JavaInstallList::getLoadTask() +{ + load(); + return getCurrentTask(); +} + +Task::Ptr JavaInstallList::getCurrentTask() +{ + if(m_status == Status::InProgress) + { + return m_loadTask; + } + return nullptr; +} + +void JavaInstallList::load() +{ + if(m_status != Status::InProgress) + { + m_status = Status::InProgress; + m_loadTask = new JavaListLoadTask(this); + m_loadTask->start(); + } +} + +const BaseVersionPtr JavaInstallList::at(int i) const +{ + return m_vlist.at(i); +} + +bool JavaInstallList::isLoaded() +{ + return m_status == JavaInstallList::Status::Done; +} + +int JavaInstallList::count() const +{ + return m_vlist.count(); +} + +QVariant JavaInstallList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + auto version = std::dynamic_pointer_cast(m_vlist[index.row()]); + switch (role) + { + case VersionPointerRole: + return qVariantFromValue(m_vlist[index.row()]); + case VersionIdRole: + return version->descriptor(); + case VersionRole: + return version->id.toString(); + case RecommendedRole: + return version->recommended; + case PathRole: + return version->path; + case ArchitectureRole: + return version->arch; + default: + return QVariant(); + } +} + +BaseVersionList::RoleList JavaInstallList::providesRoles() const +{ + return {VersionPointerRole, VersionIdRole, VersionRole, RecommendedRole, PathRole, ArchitectureRole}; +} + + +void JavaInstallList::updateListData(QList versions) +{ + beginResetModel(); + m_vlist = versions; + sortVersions(); + if(m_vlist.size()) + { + auto best = std::dynamic_pointer_cast(m_vlist[0]); + best->recommended = true; + } + endResetModel(); + m_status = Status::Done; + m_loadTask.reset(); +} + +bool sortJavas(BaseVersionPtr left, BaseVersionPtr right) +{ + auto rleft = std::dynamic_pointer_cast(left); + auto rright = std::dynamic_pointer_cast(right); + return (*rleft) > (*rright); +} + +void JavaInstallList::sortVersions() +{ + beginResetModel(); + std::sort(m_vlist.begin(), m_vlist.end(), sortJavas); + endResetModel(); +} + +JavaListLoadTask::JavaListLoadTask(JavaInstallList *vlist) : Task() +{ + m_list = vlist; + m_currentRecommended = NULL; +} + +JavaListLoadTask::~JavaListLoadTask() +{ +} + +void JavaListLoadTask::executeTask() +{ + setStatus(tr("Detecting Java installations...")); + + JavaUtils ju; + QList candidate_paths = ju.FindJavaPaths(); + + m_job = new JavaCheckerJob("Java detection"); + connect(m_job.get(), &Task::finished, this, &JavaListLoadTask::javaCheckerFinished); + connect(m_job.get(), &Task::progress, this, &Task::setProgress); + + qDebug() << "Probing the following Java paths: "; + int id = 0; + for(QString candidate : candidate_paths) + { + qDebug() << " " << candidate; + + auto candidate_checker = new JavaChecker(); + candidate_checker->m_path = candidate; + candidate_checker->m_id = id; + m_job->addJavaCheckerAction(JavaCheckerPtr(candidate_checker)); + + id++; + } + + m_job->start(); +} + +void JavaListLoadTask::javaCheckerFinished() +{ + QList candidates; + auto results = m_job->getResults(); + + qDebug() << "Found the following valid Java installations:"; + for(JavaCheckResult result : results) + { + if(result.validity == JavaCheckResult::Validity::Valid) + { + JavaInstallPtr javaVersion(new JavaInstall()); + + javaVersion->id = result.javaVersion; + javaVersion->arch = result.mojangPlatform; + javaVersion->path = result.path; + candidates.append(javaVersion); + + qDebug() << " " << javaVersion->id.toString() << javaVersion->arch << javaVersion->path; + } + } + + QList javas_bvp; + for (auto java : candidates) + { + //qDebug() << java->id << java->arch << " at " << java->path; + BaseVersionPtr bp_java = std::dynamic_pointer_cast(java); + + if (bp_java) + { + javas_bvp.append(java); + } + } + + m_list->updateListData(javas_bvp); + emitSucceeded(); +} diff --git a/ultimmc/launcher/java/JavaInstallList.h b/ultimmc/launcher/java/JavaInstallList.h new file mode 100644 index 0000000..3c237ed --- /dev/null +++ b/ultimmc/launcher/java/JavaInstallList.h @@ -0,0 +1,81 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "BaseVersionList.h" +#include "tasks/Task.h" + +#include "JavaCheckerJob.h" +#include "JavaInstall.h" + +#include "QObjectPtr.h" + +class JavaListLoadTask; + +class JavaInstallList : public BaseVersionList +{ + Q_OBJECT + enum class Status + { + NotDone, + InProgress, + Done + }; +public: + explicit JavaInstallList(QObject *parent = 0); + + Task::Ptr getLoadTask() override; + bool isLoaded() override; + const BaseVersionPtr at(int i) const override; + int count() const override; + void sortVersions() override; + + QVariant data(const QModelIndex &index, int role) const override; + RoleList providesRoles() const override; + +public slots: + void updateListData(QList versions) override; + +protected: + void load(); + Task::Ptr getCurrentTask(); + +protected: + Status m_status = Status::NotDone; + shared_qobject_ptr m_loadTask; + QList m_vlist; +}; + +class JavaListLoadTask : public Task +{ + Q_OBJECT + +public: + explicit JavaListLoadTask(JavaInstallList *vlist); + virtual ~JavaListLoadTask(); + + void executeTask() override; +public slots: + void javaCheckerFinished(); + +protected: + shared_qobject_ptr m_job; + JavaInstallList *m_list; + JavaInstall *m_currentRecommended; +}; diff --git a/ultimmc/launcher/java/JavaUtils.cpp b/ultimmc/launcher/java/JavaUtils.cpp new file mode 100644 index 0000000..fd7e43e --- /dev/null +++ b/ultimmc/launcher/java/JavaUtils.cpp @@ -0,0 +1,422 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include + +#include +#include "java/JavaUtils.h" +#include "java/JavaInstallList.h" +#include "FileSystem.h" + +#define IBUS "@im=ibus" + +JavaUtils::JavaUtils() +{ +} + +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) +static QString processLD_LIBRARY_PATH(const QString & LD_LIBRARY_PATH) +{ + QDir mmcBin(QCoreApplication::applicationDirPath()); + auto items = LD_LIBRARY_PATH.split(':'); + QStringList final; + for(auto & item: items) + { + QDir test(item); + if(test == mmcBin) + { + qDebug() << "Env:LD_LIBRARY_PATH ignoring path" << item; + continue; + } + final.append(item); + } + return final.join(':'); +} +#endif + +QProcessEnvironment CleanEnviroment() +{ + // prepare the process environment + QProcessEnvironment rawenv = QProcessEnvironment::systemEnvironment(); + QProcessEnvironment env; + + QStringList ignored = + { + "JAVA_ARGS", + "CLASSPATH", + "CONFIGPATH", + "JAVA_HOME", + "JRE_HOME", + "_JAVA_OPTIONS", + "JAVA_OPTIONS", + "JAVA_TOOL_OPTIONS" + }; + for(auto key: rawenv.keys()) + { + auto value = rawenv.value(key); + // filter out dangerous java crap + if(ignored.contains(key)) + { + qDebug() << "Env: ignoring" << key << value; + continue; + } + // filter MultiMC-related things + if(key.startsWith("QT_")) + { + qDebug() << "Env: ignoring" << key << value; + continue; + } +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + // Do not pass LD_* variables to java. They were intended for MultiMC + if(key.startsWith("LD_")) + { + qDebug() << "Env: ignoring" << key << value; + continue; + } + // Strip IBus + // IBus is a Linux IME framework. For some reason, it breaks MC? + if (key == "XMODIFIERS" && value.contains(IBUS)) + { + QString save = value; + value.replace(IBUS, ""); + qDebug() << "Env: stripped" << IBUS << "from" << save << ":" << value; + } + if(key == "GAME_PRELOAD") + { + env.insert("LD_PRELOAD", value); + continue; + } + if(key == "GAME_LIBRARY_PATH") + { + env.insert("LD_LIBRARY_PATH", processLD_LIBRARY_PATH(value)); + continue; + } +#endif + // qDebug() << "Env: " << key << value; + env.insert(key, value); + } +#ifdef Q_OS_LINUX + // HACK: Workaround for QTBUG42500 + if(!env.contains("LD_LIBRARY_PATH")) + { + env.insert("LD_LIBRARY_PATH", ""); + } +#endif + + return env; +} + +JavaInstallPtr JavaUtils::MakeJavaPtr(QString path, QString id, QString arch) +{ + JavaInstallPtr javaVersion(new JavaInstall()); + + javaVersion->id = id; + javaVersion->arch = arch; + javaVersion->path = path; + + return javaVersion; +} + +JavaInstallPtr JavaUtils::GetDefaultJava() +{ + JavaInstallPtr javaVersion(new JavaInstall()); + + javaVersion->id = "java"; + javaVersion->arch = "unknown"; +#if defined(Q_OS_WIN32) + javaVersion->path = "javaw"; +#else + javaVersion->path = "java"; +#endif + + return javaVersion; +} + +#if defined(Q_OS_WIN32) +QList JavaUtils::FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix) +{ + QList javas; + + QString archType = "unknown"; + if (keyType == KEY_WOW64_64KEY) + archType = "64"; + else if (keyType == KEY_WOW64_32KEY) + archType = "32"; + + HKEY jreKey; + if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, keyName.toStdString().c_str(), 0, + KEY_READ | keyType | KEY_ENUMERATE_SUB_KEYS, &jreKey) == ERROR_SUCCESS) + { + // Read the current type version from the registry. + // This will be used to find any key that contains the JavaHome value. + char *value = new char[0]; + DWORD valueSz = 0; + if (RegQueryValueExA(jreKey, "CurrentVersion", NULL, NULL, (BYTE *)value, &valueSz) == + ERROR_MORE_DATA) + { + value = new char[valueSz]; + RegQueryValueExA(jreKey, "CurrentVersion", NULL, NULL, (BYTE *)value, &valueSz); + } + + TCHAR subKeyName[255]; + DWORD subKeyNameSize, numSubKeys, retCode; + + // Get the number of subkeys + RegQueryInfoKey(jreKey, NULL, NULL, NULL, &numSubKeys, NULL, NULL, NULL, NULL, NULL, + NULL, NULL); + + // Iterate until RegEnumKeyEx fails + if (numSubKeys > 0) + { + for (DWORD i = 0; i < numSubKeys; i++) + { + subKeyNameSize = 255; + retCode = RegEnumKeyEx(jreKey, i, subKeyName, &subKeyNameSize, NULL, NULL, NULL, + NULL); + if (retCode == ERROR_SUCCESS) + { + // Now open the registry key for the version that we just got. + QString newKeyName = keyName + "\\" + subKeyName + subkeySuffix; + + HKEY newKey; + if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, newKeyName.toStdString().c_str(), 0, + KEY_READ | KEY_WOW64_64KEY, &newKey) == ERROR_SUCCESS) + { + // Read the JavaHome value to find where Java is installed. + value = new char[0]; + valueSz = 0; + if (RegQueryValueEx(newKey, keyJavaDir.toStdString().c_str(), NULL, NULL, (BYTE *)value, + &valueSz) == ERROR_MORE_DATA) + { + value = new char[valueSz]; + RegQueryValueEx(newKey, keyJavaDir.toStdString().c_str(), NULL, NULL, (BYTE *)value, + &valueSz); + + // Now, we construct the version object and add it to the list. + JavaInstallPtr javaVersion(new JavaInstall()); + + javaVersion->id = subKeyName; + javaVersion->arch = archType; + javaVersion->path = + QDir(FS::PathCombine(value, "bin")).absoluteFilePath("javaw.exe"); + javas.append(javaVersion); + } + + RegCloseKey(newKey); + } + } + } + } + + RegCloseKey(jreKey); + } + + return javas; +} + +QList JavaUtils::FindJavaPaths() +{ + QList java_candidates; + + // Oracle + QList JRE64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome"); + QList JDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome"); + QList JRE32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Runtime Environment", "JavaHome"); + QList JDK32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\Java Development Kit", "JavaHome"); + + // Oracle for Java 9 and newer + QList NEWJRE64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome"); + QList NEWJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome"); + QList NEWJRE32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JRE", "JavaHome"); + QList NEWJDK32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\JavaSoft\\JDK", "JavaHome"); + + // AdoptOpenJDK + QList ADOPTOPENJRE32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI"); + QList ADOPTOPENJRE64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JRE", "Path", "\\hotspot\\MSI"); + QList ADOPTOPENJDK32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI"); + QList ADOPTOPENJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\AdoptOpenJDK\\JDK", "Path", "\\hotspot\\MSI"); + + // Eclipse Foundation + QList FOUNDATIONJDK32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Foundation\\JDK", "Path", "\\hotspot\\MSI"); + QList FOUNDATIONJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Foundation\\JDK", "Path", "\\hotspot\\MSI"); + + // Eclipse Adoptium + QList ADOPTIUMJRE32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Adoptium\\JRE", "Path", "\\hotspot\\MSI"); + QList ADOPTIUMJRE64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JRE", "Path", "\\hotspot\\MSI"); + QList ADOPTIUMJDK32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI"); + QList ADOPTIUMJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\Eclipse Adoptium\\JDK", "Path", "\\hotspot\\MSI"); + + // Microsoft + QList MICROSOFTJDK64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\Microsoft\\JDK", "Path", "\\hotspot\\MSI"); + + // Azul Zulu + QList ZULU64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath"); + QList ZULU32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\Azul Systems\\Zulu", "InstallationPath"); + + // BellSoft Liberica + QList LIBERICA64s = this->FindJavaFromRegistryKey( + KEY_WOW64_64KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath"); + QList LIBERICA32s = this->FindJavaFromRegistryKey( + KEY_WOW64_32KEY, "SOFTWARE\\BellSoft\\Liberica", "InstallationPath"); + + // List x64 before x86 + java_candidates.append(JRE64s); + java_candidates.append(NEWJRE64s); + java_candidates.append(ADOPTOPENJRE64s); + java_candidates.append(ADOPTIUMJRE64s); + java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre8/bin/javaw.exe")); + java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre7/bin/javaw.exe")); + java_candidates.append(MakeJavaPtr("C:/Program Files/Java/jre6/bin/javaw.exe")); + java_candidates.append(JDK64s); + java_candidates.append(NEWJDK64s); + java_candidates.append(ADOPTOPENJDK64s); + java_candidates.append(FOUNDATIONJDK64s); + java_candidates.append(ADOPTIUMJDK64s); + java_candidates.append(MICROSOFTJDK64s); + java_candidates.append(ZULU64s); + java_candidates.append(LIBERICA64s); + + java_candidates.append(JRE32s); + java_candidates.append(NEWJRE32s); + java_candidates.append(ADOPTOPENJRE32s); + java_candidates.append(ADOPTIUMJRE32s); + java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre8/bin/javaw.exe")); + java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre7/bin/javaw.exe")); + java_candidates.append(MakeJavaPtr("C:/Program Files (x86)/Java/jre6/bin/javaw.exe")); + java_candidates.append(JDK32s); + java_candidates.append(NEWJDK32s); + java_candidates.append(ADOPTOPENJDK32s); + java_candidates.append(FOUNDATIONJDK32s); + java_candidates.append(ADOPTIUMJDK32s); + java_candidates.append(ZULU32s); + java_candidates.append(LIBERICA32s); + + java_candidates.append(MakeJavaPtr(this->GetDefaultJava()->path)); + + QList candidates; + for(JavaInstallPtr java_candidate : java_candidates) + { + if(!candidates.contains(java_candidate->path)) + { + candidates.append(java_candidate->path); + } + } + + return candidates; +} + +#elif defined(Q_OS_MAC) +QList JavaUtils::FindJavaPaths() +{ + QList javas; + javas.append(this->GetDefaultJava()->path); + javas.append("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"); + javas.append("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"); + javas.append("/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java"); + QDir libraryJVMDir("/Library/Java/JavaVirtualMachines/"); + QStringList libraryJVMJavas = libraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + foreach (const QString &java, libraryJVMJavas) { + javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); + javas.append(libraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/jre/bin/java"); + } + QDir systemLibraryJVMDir("/System/Library/Java/JavaVirtualMachines/"); + QStringList systemLibraryJVMJavas = systemLibraryJVMDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + foreach (const QString &java, systemLibraryJVMJavas) { + javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Home/bin/java"); + javas.append(systemLibraryJVMDir.absolutePath() + "/" + java + "/Contents/Commands/java"); + } + return javas; +} + +#elif defined(Q_OS_LINUX) +QList JavaUtils::FindJavaPaths() +{ + qDebug() << "Linux Java detection incomplete - defaulting to \"java\""; + + QList javas; + javas.append(this->GetDefaultJava()->path); + auto scanJavaDir = [&](const QString & dirPath) + { + QDir dir(dirPath); + if(!dir.exists()) + return; + auto entries = dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks); + for(auto & entry: entries) + { + + QString prefix; + if(entry.isAbsolute()) + { + prefix = entry.absoluteFilePath(); + } + else + { + prefix = entry.filePath(); + } + + javas.append(FS::PathCombine(prefix, "jre/bin/java")); + javas.append(FS::PathCombine(prefix, "bin/java")); + } + }; + // oracle RPMs + scanJavaDir("/usr/java"); + // general locations used by distro packaging + scanJavaDir("/usr/lib/jvm"); + scanJavaDir("/usr/lib64/jvm"); + scanJavaDir("/usr/lib32/jvm"); + // javas stored in MultiMC's folder + scanJavaDir("java"); + // manually installed JDKs in /opt + scanJavaDir("/opt/jdk"); + scanJavaDir("/opt/jdks"); + return javas; +} +#else +QList JavaUtils::FindJavaPaths() +{ + qDebug() << "Unknown operating system build - defaulting to \"java\""; + + QList javas; + javas.append(this->GetDefaultJava()->path); + + return javas; +} +#endif diff --git a/ultimmc/launcher/java/JavaUtils.h b/ultimmc/launcher/java/JavaUtils.h new file mode 100644 index 0000000..3152d14 --- /dev/null +++ b/ultimmc/launcher/java/JavaUtils.h @@ -0,0 +1,42 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "JavaChecker.h" +#include "JavaInstallList.h" + +#ifdef Q_OS_WIN +#include +#endif + +QProcessEnvironment CleanEnviroment(); + +class JavaUtils : public QObject +{ + Q_OBJECT +public: + JavaUtils(); + + JavaInstallPtr MakeJavaPtr(QString path, QString id = "unknown", QString arch = "unknown"); + QList FindJavaPaths(); + JavaInstallPtr GetDefaultJava(); + +#ifdef Q_OS_WIN + QList FindJavaFromRegistryKey(DWORD keyType, QString keyName, QString keyJavaDir, QString subkeySuffix = ""); +#endif +}; diff --git a/ultimmc/launcher/java/JavaVersion.cpp b/ultimmc/launcher/java/JavaVersion.cpp new file mode 100644 index 0000000..179ccd8 --- /dev/null +++ b/ultimmc/launcher/java/JavaVersion.cpp @@ -0,0 +1,121 @@ +#include "JavaVersion.h" +#include + +#include +#include + +JavaVersion & JavaVersion::operator=(const QString & javaVersionString) +{ + m_string = javaVersionString; + + auto getCapturedInteger = [](const QRegularExpressionMatch & match, const QString &what) -> int + { + auto str = match.captured(what); + if(str.isEmpty()) + { + return 0; + } + return str.toInt(); + }; + + QRegularExpression pattern; + if(javaVersionString.startsWith("1.")) + { + pattern = QRegularExpression ("1[.](?[0-9]+)([.](?[0-9]+))?(_(?[0-9]+)?)?(-(?[a-zA-Z0-9]+))?"); + } + else + { + pattern = QRegularExpression("(?[0-9]+)([.](?[0-9]+))?([.](?[0-9]+))?(-(?[a-zA-Z0-9]+))?"); + } + + auto match = pattern.match(m_string); + m_parseable = match.hasMatch(); + m_major = getCapturedInteger(match, "major"); + m_minor = getCapturedInteger(match, "minor"); + m_security = getCapturedInteger(match, "security"); + m_prerelease = match.captured("prerelease"); + return *this; +} + +JavaVersion::JavaVersion(const QString &rhs) +{ + operator=(rhs); +} + +QString JavaVersion::toString() +{ + return m_string; +} + +bool JavaVersion::requiresPermGen() +{ + if(m_parseable) + { + return m_major < 8; + } + return true; +} + +bool JavaVersion::operator<(const JavaVersion &rhs) +{ + if(m_parseable && rhs.m_parseable) + { + auto major = m_major; + auto rmajor = rhs.m_major; + + // HACK: discourage using java 9 + if(major > 8) + major = -major; + if(rmajor > 8) + rmajor = -rmajor; + + if(major < rmajor) + return true; + if(major > rmajor) + return false; + if(m_minor < rhs.m_minor) + return true; + if(m_minor > rhs.m_minor) + return false; + if(m_security < rhs.m_security) + return true; + if(m_security > rhs.m_security) + return false; + + // everything else being equal, consider prerelease status + bool thisPre = !m_prerelease.isEmpty(); + bool rhsPre = !rhs.m_prerelease.isEmpty(); + if(thisPre && !rhsPre) + { + // this is a prerelease and the other one isn't -> lesser + return true; + } + else if(!thisPre && rhsPre) + { + // this isn't a prerelease and the other one is -> greater + return false; + } + else if(thisPre && rhsPre) + { + // both are prereleases - use natural compare... + return Strings::naturalCompare(m_prerelease, rhs.m_prerelease, Qt::CaseSensitive) < 0; + } + // neither is prerelease, so they are the same -> this cannot be less than rhs + return false; + } + else return Strings::naturalCompare(m_string, rhs.m_string, Qt::CaseSensitive) < 0; +} + +bool JavaVersion::operator==(const JavaVersion &rhs) +{ + if(m_parseable && rhs.m_parseable) + { + return m_major == rhs.m_major && m_minor == rhs.m_minor && m_security == rhs.m_security && m_prerelease == rhs.m_prerelease; + } + return m_string == rhs.m_string; +} + +bool JavaVersion::operator>(const JavaVersion &rhs) +{ + return (!operator<(rhs)) && (!operator==(rhs)); +} diff --git a/ultimmc/launcher/java/JavaVersion.h b/ultimmc/launcher/java/JavaVersion.h new file mode 100644 index 0000000..9bbf064 --- /dev/null +++ b/ultimmc/launcher/java/JavaVersion.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +// NOTE: apparently the GNU C library pollutes the global namespace with these... undef them. +#ifdef major + #undef major +#endif +#ifdef minor + #undef minor +#endif + +class JavaVersion +{ + friend class JavaVersionTest; +public: + JavaVersion() {}; + JavaVersion(const QString & rhs); + + JavaVersion & operator=(const QString & rhs); + + bool operator<(const JavaVersion & rhs); + bool operator==(const JavaVersion & rhs); + bool operator>(const JavaVersion & rhs); + + bool requiresPermGen(); + + QString toString(); + + int major() + { + return m_major; + } + int minor() + { + return m_minor; + } + int security() + { + return m_security; + } +private: + QString m_string; + int m_major = 0; + int m_minor = 0; + int m_security = 0; + bool m_parseable = false; + QString m_prerelease; +}; diff --git a/ultimmc/launcher/java/JavaVersion_test.cpp b/ultimmc/launcher/java/JavaVersion_test.cpp new file mode 100644 index 0000000..10ae13a --- /dev/null +++ b/ultimmc/launcher/java/JavaVersion_test.cpp @@ -0,0 +1,116 @@ +#include +#include "TestUtil.h" + +#include "java/JavaVersion.h" + +class JavaVersionTest : public QObject +{ + Q_OBJECT +private +slots: + void test_Parse_data() + { + QTest::addColumn("string"); + QTest::addColumn("major"); + QTest::addColumn("minor"); + QTest::addColumn("security"); + QTest::addColumn("prerelease"); + + QTest::newRow("old format") << "1.6.0_33" << 6 << 0 << 33 << QString(); + QTest::newRow("old format prerelease") << "1.9.0_1-ea" << 9 << 0 << 1 << "ea"; + + QTest::newRow("new format major") << "9" << 9 << 0 << 0 << QString(); + QTest::newRow("new format minor") << "9.1" << 9 << 1 << 0 << QString(); + QTest::newRow("new format security") << "9.0.1" << 9 << 0 << 1 << QString(); + QTest::newRow("new format prerelease") << "9-ea" << 9 << 0 << 0 << "ea"; + QTest::newRow("new format long prerelease") << "9.0.1-ea" << 9 << 0 << 1 << "ea"; + } + void test_Parse() + { + QFETCH(QString, string); + QFETCH(int, major); + QFETCH(int, minor); + QFETCH(int, security); + QFETCH(QString, prerelease); + + JavaVersion test(string); + QCOMPARE(test.m_string, string); + QCOMPARE(test.toString(), string); + QCOMPARE(test.m_major, major); + QCOMPARE(test.m_minor, minor); + QCOMPARE(test.m_security, security); + QCOMPARE(test.m_prerelease, prerelease); + } + + void test_Sort_data() + { + QTest::addColumn("lhs"); + QTest::addColumn("rhs"); + QTest::addColumn("smaller"); + QTest::addColumn("equal"); + QTest::addColumn("bigger"); + + // old format and new format equivalence + QTest::newRow("1.6.0_33 == 6.0.33") << "1.6.0_33" << "6.0.33" << false << true << false; + // old format major version + QTest::newRow("1.5.0_33 < 1.6.0_33") << "1.5.0_33" << "1.6.0_33" << true << false << false; + // new format - first release vs first security patch + QTest::newRow("9 < 9.0.1") << "9" << "9.0.1" << true << false << false; + QTest::newRow("9.0.1 > 9") << "9.0.1" << "9" << false << false << true; + // new format - first minor vs first release/security patch + QTest::newRow("9.1 > 9.0.1") << "9.1" << "9.0.1" << false << false << true; + QTest::newRow("9.0.1 < 9.1") << "9.0.1" << "9.1" << true << false << false; + QTest::newRow("9.1 > 9") << "9.1" << "9" << false << false << true; + QTest::newRow("9 > 9.1") << "9" << "9.1" << true << false << false; + // new format - omitted numbers + QTest::newRow("9 == 9.0") << "9" << "9.0" << false << true << false; + QTest::newRow("9 == 9.0.0") << "9" << "9.0.0" << false << true << false; + QTest::newRow("9.0 == 9.0.0") << "9.0" << "9.0.0" << false << true << false; + // early access and prereleases compared to final release + QTest::newRow("9-ea < 9") << "9-ea" << "9" << true << false << false; + QTest::newRow("9 < 9.0.1-ea") << "9" << "9.0.1-ea" << true << false << false; + QTest::newRow("9.0.1-ea > 9") << "9.0.1-ea" << "9" << false << false << true; + // prerelease difference only testing + QTest::newRow("9-1 == 9-1") << "9-1" << "9-1" << false << true << false; + QTest::newRow("9-1 < 9-2") << "9-1" << "9-2" << true << false << false; + QTest::newRow("9-5 < 9-20") << "9-5" << "9-20" << true << false << false; + QTest::newRow("9-rc1 < 9-rc2") << "9-rc1" << "9-rc2" << true << false << false; + QTest::newRow("9-rc5 < 9-rc20") << "9-rc5" << "9-rc20" << true << false << false; + QTest::newRow("9-rc < 9-rc2") << "9-rc" << "9-rc2" << true << false << false; + QTest::newRow("9-ea < 9-rc") << "9-ea" << "9-rc" << true << false << false; + } + void test_Sort() + { + QFETCH(QString, lhs); + QFETCH(QString, rhs); + QFETCH(bool, smaller); + QFETCH(bool, equal); + QFETCH(bool, bigger); + JavaVersion lver(lhs); + JavaVersion rver(rhs); + QCOMPARE(lver < rver, smaller); + QCOMPARE(lver == rver, equal); + QCOMPARE(lver > rver, bigger); + } + void test_PermGen_data() + { + QTest::addColumn("version"); + QTest::addColumn("needs_permgen"); + QTest::newRow("1.6.0_33") << "1.6.0_33" << true; + QTest::newRow("1.7.0_60") << "1.7.0_60" << true; + QTest::newRow("1.8.0_22") << "1.8.0_22" << false; + QTest::newRow("9-ea") << "9-ea" << false; + QTest::newRow("9.2.4") << "9.2.4" << false; + } + void test_PermGen() + { + QFETCH(QString, version); + QFETCH(bool, needs_permgen); + JavaVersion v(version); + QCOMPARE(needs_permgen, v.requiresPermGen()); + } +}; + +QTEST_GUILESS_MAIN(JavaVersionTest) + +#include "JavaVersion_test.moc" diff --git a/ultimmc/launcher/launch/LaunchStep.cpp b/ultimmc/launcher/launch/LaunchStep.cpp new file mode 100644 index 0000000..d6bb6e8 --- /dev/null +++ b/ultimmc/launcher/launch/LaunchStep.cpp @@ -0,0 +1,27 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LaunchStep.h" +#include "LaunchTask.h" + +void LaunchStep::bind(LaunchTask *parent) +{ + m_parent = parent; + connect(this, &LaunchStep::readyForLaunch, parent, &LaunchTask::onReadyForLaunch); + connect(this, &LaunchStep::logLine, parent, &LaunchTask::onLogLine); + connect(this, &LaunchStep::logLines, parent, &LaunchTask::onLogLines); + connect(this, &LaunchStep::finished, parent, &LaunchTask::onStepFinished); + connect(this, &LaunchStep::progressReportingRequest, parent, &LaunchTask::onProgressReportingRequested); +} diff --git a/ultimmc/launcher/launch/LaunchStep.h b/ultimmc/launcher/launch/LaunchStep.h new file mode 100644 index 0000000..3939f96 --- /dev/null +++ b/ultimmc/launcher/launch/LaunchStep.h @@ -0,0 +1,50 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tasks/Task.h" +#include "MessageLevel.h" + +#include + +class LaunchTask; +class LaunchStep: public Task +{ + Q_OBJECT +public: /* methods */ + explicit LaunchStep(LaunchTask *parent):Task(nullptr), m_parent(parent) + { + bind(parent); + }; + virtual ~LaunchStep() {}; + +private: /* methods */ + void bind(LaunchTask *parent); + +signals: + void logLines(QStringList lines, MessageLevel::Enum level); + void logLine(QString line, MessageLevel::Enum level); + void readyForLaunch(); + void progressReportingRequest(); + +public slots: + virtual void proceed() {}; + // called in the opposite order than the Task launch(), used to clean up or otherwise undo things after the launch ends + virtual void finalize() {}; + +protected: /* data */ + LaunchTask *m_parent; +}; diff --git a/ultimmc/launcher/launch/LaunchTask.cpp b/ultimmc/launcher/launch/LaunchTask.cpp new file mode 100644 index 0000000..e6f6bba --- /dev/null +++ b/ultimmc/launcher/launch/LaunchTask.cpp @@ -0,0 +1,280 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Authors: Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "launch/LaunchTask.h" +#include "MessageLevel.h" +#include "MMCStrings.h" +#include "java/JavaChecker.h" +#include "tasks/Task.h" +#include +#include +#include +#include +#include +#include +#include + +void LaunchTask::init() +{ + m_instance->setRunning(true); +} + +shared_qobject_ptr LaunchTask::create(InstancePtr inst) +{ + shared_qobject_ptr proc(new LaunchTask(inst)); + proc->init(); + return proc; +} + +LaunchTask::LaunchTask(InstancePtr instance): m_instance(instance) +{ +} + +void LaunchTask::appendStep(shared_qobject_ptr step) +{ + m_steps.append(step); +} + +void LaunchTask::prependStep(shared_qobject_ptr step) +{ + m_steps.prepend(step); +} + +void LaunchTask::executeTask() +{ + m_instance->setCrashed(false); + if(!m_steps.size()) + { + state = LaunchTask::Finished; + emitSucceeded(); + } + state = LaunchTask::Running; + onStepFinished(); +} + +void LaunchTask::onReadyForLaunch() +{ + state = LaunchTask::Waiting; + emit readyForLaunch(); +} + +void LaunchTask::onStepFinished() +{ + // initial -> just start the first step + if(currentStep == -1) + { + currentStep ++; + m_steps[currentStep]->start(); + return; + } + + auto step = m_steps[currentStep]; + if(step->wasSuccessful()) + { + // end? + if(currentStep == m_steps.size() - 1) + { + finalizeSteps(true, QString()); + } + else + { + currentStep ++; + step = m_steps[currentStep]; + step->start(); + } + } + else + { + finalizeSteps(false, step->failReason()); + } +} + +void LaunchTask::finalizeSteps(bool successful, const QString& error) +{ + for(auto step = currentStep; step >= 0; step--) + { + m_steps[step]->finalize(); + } + if(successful) + { + emitSucceeded(); + } + else + { + emitFailed(error); + } +} + +void LaunchTask::onProgressReportingRequested() +{ + state = LaunchTask::Waiting; + emit requestProgress(m_steps[currentStep].get()); +} + +void LaunchTask::setCensorFilter(QMap filter) +{ + m_censorFilter = filter; +} + +QString LaunchTask::censorPrivateInfo(QString in) +{ + auto iter = m_censorFilter.begin(); + while (iter != m_censorFilter.end()) + { + in.replace(iter.key(), iter.value()); + iter++; + } + return in; +} + +void LaunchTask::proceed() +{ + if(state != LaunchTask::Waiting) + { + return; + } + m_steps[currentStep]->proceed(); +} + +bool LaunchTask::canAbort() const +{ + switch(state) + { + case LaunchTask::Aborted: + case LaunchTask::Failed: + case LaunchTask::Finished: + return false; + case LaunchTask::NotStarted: + return true; + case LaunchTask::Running: + case LaunchTask::Waiting: + { + auto step = m_steps[currentStep]; + return step->canAbort(); + } + } + return false; +} + +bool LaunchTask::abort() +{ + switch(state) + { + case LaunchTask::Aborted: + case LaunchTask::Failed: + case LaunchTask::Finished: + return true; + case LaunchTask::NotStarted: + { + state = LaunchTask::Aborted; + emitFailed("Aborted"); + return true; + } + case LaunchTask::Running: + case LaunchTask::Waiting: + { + auto step = m_steps[currentStep]; + if(!step->canAbort()) + { + return false; + } + if(step->abort()) + { + state = LaunchTask::Aborted; + return true; + } + } + default: + break; + } + return false; +} + +shared_qobject_ptr LaunchTask::getLogModel() +{ + if(!m_logModel) + { + m_logModel.reset(new LogModel()); + m_logModel->setMaxLines(m_instance->getConsoleMaxLines()); + m_logModel->setStopOnOverflow(m_instance->shouldStopOnConsoleOverflow()); + // FIXME: should this really be here? + m_logModel->setOverflowMessage(tr("MultiMC stopped watching the game log because the log length surpassed %1 lines.\n" + "You may have to fix your mods because the game is still logging to files and" + " likely wasting harddrive space at an alarming rate!").arg(m_logModel->getMaxLines())); + } + return m_logModel; +} + +void LaunchTask::onLogLines(const QStringList &lines, MessageLevel::Enum defaultLevel) +{ + for (auto & line: lines) + { + onLogLine(line, defaultLevel); + } +} + +void LaunchTask::onLogLine(QString line, MessageLevel::Enum level) +{ + // if the launcher part set a log level, use it + auto innerLevel = MessageLevel::fromLine(line); + if(innerLevel != MessageLevel::Unknown) + { + level = innerLevel; + } + + // If the level is still undetermined, guess level + if (level == MessageLevel::StdErr || level == MessageLevel::StdOut || level == MessageLevel::Unknown) + { + level = m_instance->guessLevel(line, level); + } + + // censor private user info + line = censorPrivateInfo(line); + + auto &model = *getLogModel(); + model.append(level, line); +} + +void LaunchTask::emitSucceeded() +{ + m_instance->setRunning(false); + Task::emitSucceeded(); +} + +void LaunchTask::emitFailed(QString reason) +{ + m_instance->setRunning(false); + m_instance->setCrashed(true); + Task::emitFailed(reason); +} + +QString LaunchTask::substituteVariables(const QString &cmd) const +{ + QString out = cmd; + auto variables = m_instance->getVariables(); + for (auto it = variables.begin(); it != variables.end(); ++it) + { + out.replace("$" + it.key(), it.value()); + } + auto env = QProcessEnvironment::systemEnvironment(); + for (auto var : env.keys()) + { + out.replace("$" + var, env.value(var)); + } + return out; +} + diff --git a/ultimmc/launcher/launch/LaunchTask.h b/ultimmc/launcher/launch/LaunchTask.h new file mode 100644 index 0000000..a1e891a --- /dev/null +++ b/ultimmc/launcher/launch/LaunchTask.h @@ -0,0 +1,123 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Authors: Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include "LogModel.h" +#include "BaseInstance.h" +#include "MessageLevel.h" +#include "LoggedProcess.h" +#include "LaunchStep.h" + +class LaunchTask: public Task +{ + Q_OBJECT +protected: + explicit LaunchTask(InstancePtr instance); + void init(); + +public: + enum State + { + NotStarted, + Running, + Waiting, + Failed, + Aborted, + Finished + }; + +public: /* methods */ + static shared_qobject_ptr create(InstancePtr inst); + virtual ~LaunchTask() {}; + + void appendStep(shared_qobject_ptr step); + void prependStep(shared_qobject_ptr step); + void setCensorFilter(QMap filter); + + InstancePtr instance() + { + return m_instance; + } + + void setPid(qint64 pid) + { + m_pid = pid; + } + + qint64 pid() + { + return m_pid; + } + + /** + * @brief prepare the process for launch (for multi-stage launch) + */ + virtual void executeTask() override; + + /** + * @brief launch the armed instance + */ + void proceed(); + + /** + * @brief abort launch + */ + bool abort() override; + + bool canAbort() const override; + + shared_qobject_ptr getLogModel(); + +public: + QString substituteVariables(const QString &cmd) const; + QString censorPrivateInfo(QString in); + +protected: /* methods */ + virtual void emitFailed(QString reason) override; + virtual void emitSucceeded() override; + +signals: + /** + * @brief emitted when the launch preparations are done + */ + void readyForLaunch(); + + void requestProgress(Task *task); + + void requestLogging(); + +public slots: + void onLogLines(const QStringList& lines, MessageLevel::Enum defaultLevel = MessageLevel::Launcher); + void onLogLine(QString line, MessageLevel::Enum defaultLevel = MessageLevel::Launcher); + void onReadyForLaunch(); + void onStepFinished(); + void onProgressReportingRequested(); + +private: /*methods */ + void finalizeSteps(bool successful, const QString & error); + +protected: /* data */ + InstancePtr m_instance; + shared_qobject_ptr m_logModel; + QList > m_steps; + QMap m_censorFilter; + int currentStep = -1; + State state = NotStarted; + qint64 m_pid = -1; +}; diff --git a/ultimmc/launcher/launch/LogModel.cpp b/ultimmc/launcher/launch/LogModel.cpp new file mode 100644 index 0000000..92f9487 --- /dev/null +++ b/ultimmc/launcher/launch/LogModel.cpp @@ -0,0 +1,167 @@ +#include "LogModel.h" + +LogModel::LogModel(QObject *parent):QAbstractListModel(parent) +{ + m_content.resize(m_maxLines); +} + +int LogModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + + return m_numLines; +} + +QVariant LogModel::data(const QModelIndex &index, int role) const +{ + if (index.row() < 0 || index.row() >= m_numLines) + return QVariant(); + + auto row = index.row(); + auto realRow = (row + m_firstLine) % m_maxLines; + if (role == Qt::DisplayRole || role == Qt::EditRole) + { + return m_content[realRow].line; + } + if(role == LevelRole) + { + return m_content[realRow].level; + } + + return QVariant(); +} + +void LogModel::append(MessageLevel::Enum level, QString line) +{ + if(m_suspended) + { + return; + } + int lineNum = (m_firstLine + m_numLines) % m_maxLines; + // overflow + if(m_numLines == m_maxLines) + { + if(m_stopOnOverflow) + { + // nothing more to do, the buffer is full + return; + } + beginRemoveRows(QModelIndex(), 0, 0); + m_firstLine = (m_firstLine + 1) % m_maxLines; + m_numLines --; + endRemoveRows(); + } + else if (m_numLines == m_maxLines - 1 && m_stopOnOverflow) + { + level = MessageLevel::Fatal; + line = m_overflowMessage; + } + beginInsertRows(QModelIndex(), m_numLines, m_numLines); + m_numLines ++; + m_content[lineNum].level = level; + m_content[lineNum].line = line; + endInsertRows(); +} + +void LogModel::suspend(bool suspend) +{ + m_suspended = suspend; +} + +bool LogModel::suspended() +{ + return m_suspended; +} + +void LogModel::clear() +{ + beginResetModel(); + m_firstLine = 0; + m_numLines = 0; + endResetModel(); +} + +QString LogModel::toPlainText() +{ + QString out; + out.reserve(m_numLines * 80); + for(int i = 0; i < m_numLines; i++) + { + QString & line = m_content[(m_firstLine + i) % m_maxLines].line; + out.append(line + '\n'); + } + out.squeeze(); + return out; +} + +void LogModel::setMaxLines(int maxLines) +{ + // no-op + if(maxLines == m_maxLines) + { + return; + } + // if it all still fits in the buffer, just resize it + if(m_firstLine + m_numLines < m_maxLines) + { + m_maxLines = maxLines; + m_content.resize(maxLines); + return; + } + // otherwise, we need to reorganize the data because it crosses the wrap boundary + QVector newContent; + newContent.resize(maxLines); + if(m_numLines <= maxLines) + { + // if it all fits in the new buffer, just copy it over + for(int i = 0; i < m_numLines; i++) + { + newContent[i] = m_content[(m_firstLine + i) % m_maxLines]; + } + m_content.swap(newContent); + } + else + { + // if it doesn't fit, part of the data needs to be thrown away (the oldest log messages) + int lead = m_numLines - maxLines; + beginRemoveRows(QModelIndex(), 0, lead - 1); + for(int i = 0; i < maxLines; i++) + { + newContent[i] = m_content[(m_firstLine + lead + i) % m_maxLines]; + } + m_numLines = m_maxLines; + m_content.swap(newContent); + endRemoveRows(); + } + m_firstLine = 0; + m_maxLines = maxLines; +} + +int LogModel::getMaxLines() +{ + return m_maxLines; +} + +void LogModel::setStopOnOverflow(bool stop) +{ + m_stopOnOverflow = stop; +} + +void LogModel::setOverflowMessage(const QString& overflowMessage) +{ + m_overflowMessage = overflowMessage; +} + +void LogModel::setLineWrap(bool state) +{ + if(m_lineWrap != state) + { + m_lineWrap = state; + } +} + +bool LogModel::wrapLines() const +{ + return m_lineWrap; +} diff --git a/ultimmc/launcher/launch/LogModel.h b/ultimmc/launcher/launch/LogModel.h new file mode 100644 index 0000000..6aabc82 --- /dev/null +++ b/ultimmc/launcher/launch/LogModel.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include "MessageLevel.h" + +class LogModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit LogModel(QObject *parent = 0); + + int rowCount(const QModelIndex &parent = QModelIndex()) const; + QVariant data(const QModelIndex &index, int role) const; + + void append(MessageLevel::Enum, QString line); + void clear(); + + void suspend(bool suspend); + bool suspended(); + + QString toPlainText(); + + int getMaxLines(); + void setMaxLines(int maxLines); + void setStopOnOverflow(bool stop); + void setOverflowMessage(const QString & overflowMessage); + + void setLineWrap(bool state); + bool wrapLines() const; + + enum Roles + { + LevelRole = Qt::UserRole + }; + +private /* types */: + struct entry + { + MessageLevel::Enum level; + QString line; + }; + +private: /* data */ + QVector m_content; + int m_maxLines = 1000; + // first line in the circular buffer + int m_firstLine = 0; + // number of lines occupied in the circular buffer + int m_numLines = 0; + bool m_stopOnOverflow = false; + QString m_overflowMessage = "OVERFLOW"; + bool m_suspended = false; + bool m_lineWrap = true; + +private: + Q_DISABLE_COPY(LogModel) +}; diff --git a/ultimmc/launcher/launch/steps/CheckJava.cpp b/ultimmc/launcher/launch/steps/CheckJava.cpp new file mode 100644 index 0000000..fb33823 --- /dev/null +++ b/ultimmc/launcher/launch/steps/CheckJava.cpp @@ -0,0 +1,139 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "CheckJava.h" +#include +#include +#include +#include +#include + +void CheckJava::executeTask() +{ + auto instance = m_parent->instance(); + auto settings = instance->settings(); + m_javaPath = FS::ResolveExecutable(settings->get("JavaPath").toString()); + bool perInstance = settings->get("OverrideJava").toBool() || settings->get("OverrideJavaLocation").toBool(); + + auto realJavaPath = QStandardPaths::findExecutable(m_javaPath); + if (realJavaPath.isEmpty()) + { + if (perInstance) + { + emit logLine( + QString("The java binary \"%1\" couldn't be found. Please fix the java path " + "override in the instance's settings or disable it.").arg(m_javaPath), + MessageLevel::Warning); + } + else + { + emit logLine(QString("The java binary \"%1\" couldn't be found. Please set up java in " + "the settings.").arg(m_javaPath), + MessageLevel::Warning); + } + emitFailed(QString("Java path is not valid.")); + return; + } + else + { + emit logLine("Java path is:\n" + m_javaPath + "\n\n", MessageLevel::Launcher); + } + + QFileInfo javaInfo(realJavaPath); + qlonglong javaUnixTime = javaInfo.lastModified().toMSecsSinceEpoch(); + auto storedUnixTime = settings->get("JavaTimestamp").toLongLong(); + auto storedArchitecture = settings->get("JavaArchitecture").toString(); + auto storedVersion = settings->get("JavaVersion").toString(); + auto storedVendor = settings->get("JavaVendor").toString(); + m_javaUnixTime = javaUnixTime; + // if timestamps are not the same, or something is missing, check! + if (javaUnixTime != storedUnixTime || storedVersion.size() == 0 || storedArchitecture.size() == 0 || storedVendor.size() == 0) + { + m_JavaChecker = new JavaChecker(); + emit logLine(QString("Checking Java version..."), MessageLevel::Launcher); + connect(m_JavaChecker.get(), &JavaChecker::checkFinished, this, &CheckJava::checkJavaFinished); + m_JavaChecker->m_path = realJavaPath; + m_JavaChecker->performCheck(); + return; + } + else + { + auto verString = instance->settings()->get("JavaVersion").toString(); + auto archString = instance->settings()->get("JavaArchitecture").toString(); + auto vendorString = instance->settings()->get("JavaVendor").toString(); + printJavaInfo(verString, archString, vendorString); + } + emitSucceeded(); +} + +void CheckJava::checkJavaFinished(JavaCheckResult result) +{ + switch (result.validity) + { + case JavaCheckResult::Validity::Errored: + { + // Error message displayed if java can't start + emit logLine(QString("Could not start java:"), MessageLevel::Error); + emit logLines(result.errorLog.split('\n'), MessageLevel::Error); + emit logLine("\nCheck your MultiMC Java settings.", MessageLevel::Launcher); + printSystemInfo(false, false); + emitFailed(QString("Could not start java!")); + return; + } + case JavaCheckResult::Validity::ReturnedInvalidData: + { + emit logLine(QString("Java checker returned some invalid data MultiMC doesn't understand:"), MessageLevel::Error); + emit logLines(result.outLog.split('\n'), MessageLevel::Warning); + emit logLine("\nMinecraft might not start properly.", MessageLevel::Launcher); + printSystemInfo(false, false); + emitSucceeded(); + return; + } + case JavaCheckResult::Validity::Valid: + { + auto instance = m_parent->instance(); + printJavaInfo(result.javaVersion.toString(), result.mojangPlatform, result.javaVendor); + instance->settings()->set("JavaVersion", result.javaVersion.toString()); + instance->settings()->set("JavaArchitecture", result.mojangPlatform); + instance->settings()->set("JavaVendor", result.javaVendor); + instance->settings()->set("JavaTimestamp", m_javaUnixTime); + emitSucceeded(); + return; + } + } +} + +void CheckJava::printJavaInfo(const QString& version, const QString& architecture, const QString & vendor) +{ + emit logLine(QString("Java is version %1, using %2-bit architecture, from %3.\n\n").arg(version, architecture, vendor), MessageLevel::Launcher); + printSystemInfo(true, architecture == "64"); +} + +void CheckJava::printSystemInfo(bool javaIsKnown, bool javaIs64bit) +{ + auto cpu64 = Sys::isCPU64bit(); + auto system64 = Sys::isSystem64bit(); + if(cpu64 != system64) + { + emit logLine(QString("Your CPU architecture is not matching your system architecture. You might want to install a 64bit Operating System.\n\n"), MessageLevel::Error); + } + if(javaIsKnown) + { + if(javaIs64bit != system64) + { + emit logLine(QString("Your Java architecture is not matching your system architecture. You might want to install a 64bit Java version.\n\n"), MessageLevel::Error); + } + } +} diff --git a/ultimmc/launcher/launch/steps/CheckJava.h b/ultimmc/launcher/launch/steps/CheckJava.h new file mode 100644 index 0000000..68cd618 --- /dev/null +++ b/ultimmc/launcher/launch/steps/CheckJava.h @@ -0,0 +1,45 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +class CheckJava: public LaunchStep +{ + Q_OBJECT +public: + explicit CheckJava(LaunchTask *parent) :LaunchStep(parent){}; + virtual ~CheckJava() {}; + + virtual void executeTask(); + virtual bool canAbort() const + { + return false; + } +private slots: + void checkJavaFinished(JavaCheckResult result); + +private: + void printJavaInfo(const QString & version, const QString & architecture, const QString & vendor); + void printSystemInfo(bool javaIsKnown, bool javaIs64bit); + +private: + QString m_javaPath; + qlonglong m_javaUnixTime; + JavaCheckerPtr m_JavaChecker; +}; diff --git a/ultimmc/launcher/launch/steps/LookupServerAddress.cpp b/ultimmc/launcher/launch/steps/LookupServerAddress.cpp new file mode 100644 index 0000000..d57e8b1 --- /dev/null +++ b/ultimmc/launcher/launch/steps/LookupServerAddress.cpp @@ -0,0 +1,95 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include "LookupServerAddress.h" + +#include + +LookupServerAddress::LookupServerAddress(LaunchTask *parent) : + LaunchStep(parent), m_dnsLookup(new QDnsLookup(this)) +{ + connect(m_dnsLookup, &QDnsLookup::finished, this, &LookupServerAddress::on_dnsLookupFinished); + + m_dnsLookup->setType(QDnsLookup::SRV); +} + +void LookupServerAddress::setLookupAddress(const QString &lookupAddress) +{ + m_lookupAddress = lookupAddress; + m_dnsLookup->setName(QString("_minecraft._tcp.%1").arg(lookupAddress)); +} + +void LookupServerAddress::setOutputAddressPtr(QuickPlayTargetPtr output) +{ + m_output = std::move(output); +} + +bool LookupServerAddress::abort() +{ + m_dnsLookup->abort(); + emitFailed("Aborted"); + return true; +} + +void LookupServerAddress::executeTask() +{ + m_dnsLookup->lookup(); +} + +void LookupServerAddress::on_dnsLookupFinished() +{ + if (isFinished()) + { + // Aborted + return; + } + + if (m_dnsLookup->error() != QDnsLookup::NoError) + { + emit logLine(QString("Failed to resolve server address (this is NOT an error!) %1: %2\n") + .arg(m_dnsLookup->name(), m_dnsLookup->errorString()), MessageLevel::Launcher); + resolve(m_lookupAddress, 25565); // Technically the task failed, however, we don't abort the launch + // and leave it up to minecraft to fail (or maybe not) when connecting + return; + } + + const auto records = m_dnsLookup->serviceRecords(); + if (records.empty()) + { + emit logLine( + QString("Failed to resolve server address %1: the DNS lookup succeeded, but no records were returned.\n") + .arg(m_dnsLookup->name()), MessageLevel::Warning); + resolve(m_lookupAddress, 25565); // Technically the task failed, however, we don't abort the launch + // and leave it up to minecraft to fail (or maybe not) when connecting + return; + } + + const auto &firstRecord = records.at(0); + quint16 port = firstRecord.port(); + + emit logLine(QString("Resolved server address %1 to %2 with port %3\n").arg( + m_dnsLookup->name(), firstRecord.target(), QString::number(port)),MessageLevel::Launcher); + resolve(firstRecord.target(), port); +} + +void LookupServerAddress::resolve(const QString &address, quint16 port) +{ + m_output->address = address; + m_output->port = port; + + emitSucceeded(); + m_dnsLookup->deleteLater(); +} diff --git a/ultimmc/launcher/launch/steps/LookupServerAddress.h b/ultimmc/launcher/launch/steps/LookupServerAddress.h new file mode 100644 index 0000000..4966b30 --- /dev/null +++ b/ultimmc/launcher/launch/steps/LookupServerAddress.h @@ -0,0 +1,49 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "minecraft/launch/QuickPlayTarget.h" + +class LookupServerAddress: public LaunchStep { +Q_OBJECT +public: + explicit LookupServerAddress(LaunchTask *parent); + virtual ~LookupServerAddress() {}; + + virtual void executeTask(); + virtual bool abort(); + virtual bool canAbort() const + { + return true; + } + + void setLookupAddress(const QString &lookupAddress); + void setOutputAddressPtr(QuickPlayTargetPtr output); + +private slots: + void on_dnsLookupFinished(); + +private: + void resolve(const QString &address, quint16 port); + + QDnsLookup *m_dnsLookup; + QString m_lookupAddress; + QuickPlayTargetPtr m_output; +}; diff --git a/ultimmc/launcher/launch/steps/PostLaunchCommand.cpp b/ultimmc/launcher/launch/steps/PostLaunchCommand.cpp new file mode 100644 index 0000000..143eb44 --- /dev/null +++ b/ultimmc/launcher/launch/steps/PostLaunchCommand.cpp @@ -0,0 +1,84 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PostLaunchCommand.h" +#include + +PostLaunchCommand::PostLaunchCommand(LaunchTask *parent) : LaunchStep(parent) +{ + auto instance = m_parent->instance(); + m_command = instance->getPostExitCommand(); + m_process.setProcessEnvironment(instance->createEnvironment()); + connect(&m_process, &LoggedProcess::log, this, &PostLaunchCommand::logLines); + connect(&m_process, &LoggedProcess::stateChanged, this, &PostLaunchCommand::on_state); +} + +void PostLaunchCommand::executeTask() +{ + QString postlaunch_cmd = m_parent->substituteVariables(m_command); + emit logLine(tr("Running Post-Launch command: %1").arg(postlaunch_cmd), MessageLevel::Launcher); + m_process.start(postlaunch_cmd); +} + +void PostLaunchCommand::on_state(LoggedProcess::State state) +{ + auto getError = [&]() + { + return tr("Post-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); + }; + switch(state) + { + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + case LoggedProcess::FailedToStart: + { + auto error = getError(); + emit logLine(error, MessageLevel::Fatal); + emitFailed(error); + return; + } + case LoggedProcess::Finished: + { + if(m_process.exitCode() != 0) + { + auto error = getError(); + emit logLine(error, MessageLevel::Fatal); + emitFailed(error); + } + else + { + emit logLine(tr("Post-Launch command ran successfully.\n\n"), MessageLevel::Launcher); + emitSucceeded(); + } + } + default: + break; + } +} + +void PostLaunchCommand::setWorkingDirectory(const QString &wd) +{ + m_process.setWorkingDirectory(wd); +} + +bool PostLaunchCommand::abort() +{ + auto state = m_process.state(); + if (state == LoggedProcess::Running || state == LoggedProcess::Starting) + { + m_process.kill(); + } + return true; +} diff --git a/ultimmc/launcher/launch/steps/PostLaunchCommand.h b/ultimmc/launcher/launch/steps/PostLaunchCommand.h new file mode 100644 index 0000000..ab4c494 --- /dev/null +++ b/ultimmc/launcher/launch/steps/PostLaunchCommand.h @@ -0,0 +1,41 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +class PostLaunchCommand: public LaunchStep +{ + Q_OBJECT +public: + explicit PostLaunchCommand(LaunchTask *parent); + virtual ~PostLaunchCommand() {}; + + virtual void executeTask(); + virtual bool abort(); + virtual bool canAbort() const + { + return true; + } + void setWorkingDirectory(const QString &wd); +private slots: + void on_state(LoggedProcess::State state); + +private: + LoggedProcess m_process; + QString m_command; +}; diff --git a/ultimmc/launcher/launch/steps/PreLaunchCommand.cpp b/ultimmc/launcher/launch/steps/PreLaunchCommand.cpp new file mode 100644 index 0000000..1a0889c --- /dev/null +++ b/ultimmc/launcher/launch/steps/PreLaunchCommand.cpp @@ -0,0 +1,85 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PreLaunchCommand.h" +#include + +PreLaunchCommand::PreLaunchCommand(LaunchTask *parent) : LaunchStep(parent) +{ + auto instance = m_parent->instance(); + m_command = instance->getPreLaunchCommand(); + m_process.setProcessEnvironment(instance->createEnvironment()); + connect(&m_process, &LoggedProcess::log, this, &PreLaunchCommand::logLines); + connect(&m_process, &LoggedProcess::stateChanged, this, &PreLaunchCommand::on_state); +} + +void PreLaunchCommand::executeTask() +{ + //FIXME: where to put this? + QString prelaunch_cmd = m_parent->substituteVariables(m_command); + emit logLine(tr("Running Pre-Launch command: %1").arg(prelaunch_cmd), MessageLevel::Launcher); + m_process.start(prelaunch_cmd); +} + +void PreLaunchCommand::on_state(LoggedProcess::State state) +{ + auto getError = [&]() + { + return tr("Pre-Launch command failed with code %1.\n\n").arg(m_process.exitCode()); + }; + switch(state) + { + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + case LoggedProcess::FailedToStart: + { + auto error = getError(); + emit logLine(error, MessageLevel::Fatal); + emitFailed(error); + return; + } + case LoggedProcess::Finished: + { + if(m_process.exitCode() != 0) + { + auto error = getError(); + emit logLine(error, MessageLevel::Fatal); + emitFailed(error); + } + else + { + emit logLine(tr("Pre-Launch command ran successfully.\n\n"), MessageLevel::Launcher); + emitSucceeded(); + } + } + default: + break; + } +} + +void PreLaunchCommand::setWorkingDirectory(const QString &wd) +{ + m_process.setWorkingDirectory(wd); +} + +bool PreLaunchCommand::abort() +{ + auto state = m_process.state(); + if (state == LoggedProcess::Running || state == LoggedProcess::Starting) + { + m_process.kill(); + } + return true; +} diff --git a/ultimmc/launcher/launch/steps/PreLaunchCommand.h b/ultimmc/launcher/launch/steps/PreLaunchCommand.h new file mode 100644 index 0000000..dc069f7 --- /dev/null +++ b/ultimmc/launcher/launch/steps/PreLaunchCommand.h @@ -0,0 +1,41 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "launch/LaunchStep.h" +#include "LoggedProcess.h" + +class PreLaunchCommand: public LaunchStep +{ + Q_OBJECT +public: + explicit PreLaunchCommand(LaunchTask *parent); + virtual ~PreLaunchCommand() {}; + + virtual void executeTask(); + virtual bool abort(); + virtual bool canAbort() const + { + return true; + } + void setWorkingDirectory(const QString &wd); +private slots: + void on_state(LoggedProcess::State state); + +private: + LoggedProcess m_process; + QString m_command; +}; diff --git a/ultimmc/launcher/launch/steps/TextPrint.cpp b/ultimmc/launcher/launch/steps/TextPrint.cpp new file mode 100644 index 0000000..0c1f320 --- /dev/null +++ b/ultimmc/launcher/launch/steps/TextPrint.cpp @@ -0,0 +1,29 @@ +#include "TextPrint.h" + +TextPrint::TextPrint(LaunchTask * parent, const QStringList &lines, MessageLevel::Enum level) : LaunchStep(parent) +{ + m_lines = lines; + m_level = level; +} +TextPrint::TextPrint(LaunchTask *parent, const QString &line, MessageLevel::Enum level) : LaunchStep(parent) +{ + m_lines.append(line); + m_level = level; +} + +void TextPrint::executeTask() +{ + emit logLines(m_lines, m_level); + emitSucceeded(); +} + +bool TextPrint::canAbort() const +{ + return true; +} + +bool TextPrint::abort() +{ + emitFailed("Aborted."); + return true; +} diff --git a/ultimmc/launcher/launch/steps/TextPrint.h b/ultimmc/launcher/launch/steps/TextPrint.h new file mode 100644 index 0000000..36fa7f9 --- /dev/null +++ b/ultimmc/launcher/launch/steps/TextPrint.h @@ -0,0 +1,41 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +/* + * FIXME: maybe do not export + */ + +class TextPrint: public LaunchStep +{ + Q_OBJECT +public: + explicit TextPrint(LaunchTask *parent, const QStringList &lines, MessageLevel::Enum level); + explicit TextPrint(LaunchTask *parent, const QString &line, MessageLevel::Enum level); + virtual ~TextPrint(){}; + + virtual void executeTask(); + virtual bool canAbort() const; + virtual bool abort(); + +private: + QStringList m_lines; + MessageLevel::Enum m_level; +}; diff --git a/ultimmc/launcher/launch/steps/Update.cpp b/ultimmc/launcher/launch/steps/Update.cpp new file mode 100644 index 0000000..28bd153 --- /dev/null +++ b/ultimmc/launcher/launch/steps/Update.cpp @@ -0,0 +1,80 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Update.h" +#include + +void Update::executeTask() +{ + if(m_aborted) + { + emitFailed(tr("Task aborted.")); + return; + } + m_updateTask.reset(m_parent->instance()->createUpdateTask(m_mode)); + if(m_updateTask) + { + connect(m_updateTask.get(), SIGNAL(finished()), this, SLOT(updateFinished())); + connect(m_updateTask.get(), &Task::progress, this, &Task::setProgress); + connect(m_updateTask.get(), &Task::status, this, &Task::setStatus); + emit progressReportingRequest(); + return; + } + emitSucceeded(); +} + +void Update::proceed() +{ + m_updateTask->start(); +} + +void Update::updateFinished() +{ + if(m_updateTask->wasSuccessful()) + { + m_updateTask.reset(); + emitSucceeded(); + } + else + { + QString reason = tr("Instance update failed because: %1\n\n").arg(m_updateTask->failReason()); + m_updateTask.reset(); + emit logLine(reason, MessageLevel::Fatal); + emitFailed(reason); + } +} + +bool Update::canAbort() const +{ + if(m_updateTask) + { + return m_updateTask->canAbort(); + } + return true; +} + + +bool Update::abort() +{ + m_aborted = true; + if(m_updateTask) + { + if(m_updateTask->canAbort()) + { + return m_updateTask->abort(); + } + } + return true; +} diff --git a/ultimmc/launcher/launch/steps/Update.h b/ultimmc/launcher/launch/steps/Update.h new file mode 100644 index 0000000..ce40611 --- /dev/null +++ b/ultimmc/launcher/launch/steps/Update.h @@ -0,0 +1,45 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include + +// FIXME: stupid. should be defined by the instance type? or even completely abstracted away... +class Update: public LaunchStep +{ + Q_OBJECT +public: + explicit Update(LaunchTask *parent, Net::Mode mode):LaunchStep(parent), m_mode(mode) {}; + virtual ~Update() {}; + + void executeTask() override; + bool canAbort() const override; + void proceed() override; +public slots: + bool abort() override; + +private slots: + void updateFinished(); + +private: + Task::Ptr m_updateTask; + bool m_aborted = false; + Net::Mode m_mode = Net::Mode::Offline; +}; diff --git a/ultimmc/launcher/main.cpp b/ultimmc/launcher/main.cpp new file mode 100644 index 0000000..aabb5a0 --- /dev/null +++ b/ultimmc/launcher/main.cpp @@ -0,0 +1,59 @@ +#include "Application.h" + +// #define BREAK_INFINITE_LOOP +// #define BREAK_EXCEPTION +// #define BREAK_RETURN + +#ifdef BREAK_INFINITE_LOOP +#include +#include +#endif + +int main(int argc, char *argv[]) +{ +#ifdef BREAK_INFINITE_LOOP + while(true) + { + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + } +#endif +#ifdef BREAK_EXCEPTION + throw 42; +#endif +#ifdef BREAK_RETURN + return 42; +#endif + +#if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); +#endif + + // initialize Qt + Application app(argc, argv); + + switch (app.status()) + { + case Application::StartingUp: + case Application::Initialized: + { + Q_INIT_RESOURCE(multimc); + Q_INIT_RESOURCE(backgrounds); + Q_INIT_RESOURCE(documents); + Q_INIT_RESOURCE(logo); + + Q_INIT_RESOURCE(pe_dark); + Q_INIT_RESOURCE(pe_light); + Q_INIT_RESOURCE(pe_blue); + Q_INIT_RESOURCE(pe_colored); + Q_INIT_RESOURCE(OSX); + Q_INIT_RESOURCE(iOS); + Q_INIT_RESOURCE(flat); + return app.exec(); + } + case Application::Failed: + return 1; + case Application::Succeeded: + return 0; + } +} diff --git a/ultimmc/launcher/meta/BaseEntity.cpp b/ultimmc/launcher/meta/BaseEntity.cpp new file mode 100644 index 0000000..8415592 --- /dev/null +++ b/ultimmc/launcher/meta/BaseEntity.cpp @@ -0,0 +1,164 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "BaseEntity.h" + +#include "net/Download.h" +#include "net/HttpMetaCache.h" +#include "net/NetJob.h" +#include "Json.h" + +#include "BuildConfig.h" +#include "Application.h" + +class ParsingValidator : public Net::Validator +{ +public: /* con/des */ + ParsingValidator(Meta::BaseEntity *entity) : m_entity(entity) + { + }; + virtual ~ParsingValidator() + { + }; + +public: /* methods */ + bool init(QNetworkRequest &) override + { + return true; + } + bool write(QByteArray & data) override + { + this->data.append(data); + return true; + } + bool abort() override + { + return true; + } + bool validate(QNetworkReply &) override + { + auto fname = m_entity->localFilename(); + try + { + auto doc = Json::requireDocument(data, fname); + auto obj = Json::requireObject(doc, fname); + m_entity->parse(obj); + return true; + } + catch (const Exception &e) + { + qWarning() << "Unable to parse response:" << e.cause(); + return false; + } + } + +private: /* data */ + QByteArray data; + Meta::BaseEntity *m_entity; +}; + +Meta::BaseEntity::~BaseEntity() +{ +} + +QUrl Meta::BaseEntity::url() const +{ + return QUrl(BuildConfig.META_URL).resolved(localFilename()); +} + +bool Meta::BaseEntity::loadLocalFile() +{ + const QString fname = QDir("meta").absoluteFilePath(localFilename()); + if (!QFile::exists(fname)) + { + return false; + } + // TODO: check if the file has the expected checksum + try + { + auto doc = Json::requireDocument(fname, fname); + auto obj = Json::requireObject(doc, fname); + parse(obj); + return true; + } + catch (const Exception &e) + { + qDebug() << QString("Unable to parse file %1: %2").arg(fname, e.cause()); + // just make sure it's gone and we never consider it again. + QFile::remove(fname); + return false; + } +} + +void Meta::BaseEntity::load(Net::Mode loadType) +{ + // load local file if nothing is loaded yet + if(!isLoaded()) + { + if(loadLocalFile()) + { + m_loadStatus = LoadStatus::Local; + } + } + // if we need remote update, run the update task + if(loadType == Net::Mode::Offline || !shouldStartRemoteUpdate()) + { + return; + } + m_updateTask = new NetJob(QObject::tr("Download of meta file %1").arg(localFilename()), APPLICATION->network()); + auto url = this->url(); + auto entry = APPLICATION->metacache()->resolveEntry("meta", localFilename()); + entry->setStale(true); + auto dl = Net::Download::makeCached(url, entry); + /* + * The validator parses the file and loads it into the object. + * If that fails, the file is not written to storage. + */ + dl->addValidator(new ParsingValidator(this)); + m_updateTask->addNetAction(dl); + m_updateStatus = UpdateStatus::InProgress; + QObject::connect(m_updateTask.get(), &NetJob::succeeded, [&]() + { + m_loadStatus = LoadStatus::Remote; + m_updateStatus = UpdateStatus::Succeeded; + m_updateTask.reset(); + }); + QObject::connect(m_updateTask.get(), &NetJob::failed, [&]() + { + m_updateStatus = UpdateStatus::Failed; + m_updateTask.reset(); + }); + m_updateTask->start(); +} + +bool Meta::BaseEntity::isLoaded() const +{ + return m_loadStatus > LoadStatus::NotLoaded; +} + +bool Meta::BaseEntity::shouldStartRemoteUpdate() const +{ + // TODO: version-locks and offline mode? + return m_updateStatus != UpdateStatus::InProgress; +} + +Task::Ptr Meta::BaseEntity::getCurrentTask() +{ + if(m_updateStatus == UpdateStatus::InProgress) + { + return m_updateTask; + } + return nullptr; +} diff --git a/ultimmc/launcher/meta/BaseEntity.h b/ultimmc/launcher/meta/BaseEntity.h new file mode 100644 index 0000000..75fa384 --- /dev/null +++ b/ultimmc/launcher/meta/BaseEntity.h @@ -0,0 +1,67 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include "QObjectPtr.h" + +#include "net/Mode.h" +#include "net/NetJob.h" + +namespace Meta +{ +class BaseEntity +{ +public: /* types */ + using Ptr = std::shared_ptr; + enum class LoadStatus + { + NotLoaded, + Local, + Remote + }; + enum class UpdateStatus + { + NotDone, + InProgress, + Failed, + Succeeded + }; + +public: + virtual ~BaseEntity(); + + virtual void parse(const QJsonObject &obj) = 0; + + virtual QString localFilename() const = 0; + virtual QUrl url() const; + + bool isLoaded() const; + bool shouldStartRemoteUpdate() const; + + void load(Net::Mode loadType); + Task::Ptr getCurrentTask(); + +protected: /* methods */ + bool loadLocalFile(); + +private: + LoadStatus m_loadStatus = LoadStatus::NotLoaded; + UpdateStatus m_updateStatus = UpdateStatus::NotDone; + NetJob::Ptr m_updateTask; +}; +} diff --git a/ultimmc/launcher/meta/Index.cpp b/ultimmc/launcher/meta/Index.cpp new file mode 100644 index 0000000..6802470 --- /dev/null +++ b/ultimmc/launcher/meta/Index.cpp @@ -0,0 +1,148 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Index.h" + +#include "VersionList.h" +#include "JsonFormat.h" + +namespace Meta +{ +Index::Index(QObject *parent) + : QAbstractListModel(parent) +{ +} +Index::Index(const QVector &lists, QObject *parent) + : QAbstractListModel(parent), m_lists(lists) +{ + for (int i = 0; i < m_lists.size(); ++i) + { + m_uids.insert(m_lists.at(i)->uid(), m_lists.at(i)); + connectVersionList(i, m_lists.at(i)); + } +} + +QVariant Index::data(const QModelIndex &index, int role) const +{ + if (index.parent().isValid() || index.row() < 0 || index.row() >= m_lists.size()) + { + return QVariant(); + } + + VersionListPtr list = m_lists.at(index.row()); + switch (role) + { + case Qt::DisplayRole: + switch (index.column()) + { + case 0: return list->humanReadable(); + default: break; + } + case UidRole: return list->uid(); + case NameRole: return list->name(); + case ListPtrRole: return QVariant::fromValue(list); + } + return QVariant(); +} +int Index::rowCount(const QModelIndex &parent) const +{ + return m_lists.size(); +} +int Index::columnCount(const QModelIndex &parent) const +{ + return 1; +} +QVariant Index::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0) + { + return tr("Name"); + } + else + { + return QVariant(); + } +} + +bool Index::hasUid(const QString &uid) const +{ + return m_uids.contains(uid); +} + +VersionListPtr Index::get(const QString &uid) +{ + VersionListPtr out = m_uids.value(uid, nullptr); + if(!out) + { + out = std::make_shared(uid); + m_uids[uid] = out; + } + return out; +} + +VersionPtr Index::get(const QString &uid, const QString &version) +{ + auto list = get(uid); + return list->getVersion(version); +} + +void Index::parse(const QJsonObject& obj) +{ + parseIndex(obj, this); +} + +void Index::merge(const std::shared_ptr &other) +{ + const QVector lists = std::dynamic_pointer_cast(other)->m_lists; + // initial load, no need to merge + if (m_lists.isEmpty()) + { + beginResetModel(); + m_lists = lists; + for (int i = 0; i < lists.size(); ++i) + { + m_uids.insert(lists.at(i)->uid(), lists.at(i)); + connectVersionList(i, lists.at(i)); + } + endResetModel(); + } + else + { + for (const VersionListPtr &list : lists) + { + if (m_uids.contains(list->uid())) + { + m_uids[list->uid()]->mergeFromIndex(list); + } + else + { + beginInsertRows(QModelIndex(), m_lists.size(), m_lists.size()); + connectVersionList(m_lists.size(), list); + m_lists.append(list); + m_uids.insert(list->uid(), list); + endInsertRows(); + } + } + } +} + +void Index::connectVersionList(const int row, const VersionListPtr &list) +{ + connect(list.get(), &VersionList::nameChanged, this, [this, row]() + { + emit dataChanged(index(row), index(row), QVector() << Qt::DisplayRole); + }); +} +} diff --git a/ultimmc/launcher/meta/Index.h b/ultimmc/launcher/meta/Index.h new file mode 100644 index 0000000..d33ab0c --- /dev/null +++ b/ultimmc/launcher/meta/Index.h @@ -0,0 +1,69 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "BaseEntity.h" + +class Task; + +namespace Meta +{ +using VersionListPtr = std::shared_ptr; +using VersionPtr = std::shared_ptr; + +class Index : public QAbstractListModel, public BaseEntity +{ + Q_OBJECT +public: + explicit Index(QObject *parent = nullptr); + explicit Index(const QVector &lists, QObject *parent = nullptr); + + enum + { + UidRole = Qt::UserRole, + NameRole, + ListPtrRole + }; + + QVariant data(const QModelIndex &index, int role) const override; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + QString localFilename() const override { return "index.json"; } + + // queries + VersionListPtr get(const QString &uid); + VersionPtr get(const QString &uid, const QString &version); + bool hasUid(const QString &uid) const; + + QVector lists() const { return m_lists; } + +public: // for usage by parsers only + void merge(const std::shared_ptr &other); + void parse(const QJsonObject &obj) override; + +private: + QVector m_lists; + QHash m_uids; + + void connectVersionList(const int row, const VersionListPtr &list); +}; +} + diff --git a/ultimmc/launcher/meta/Index_test.cpp b/ultimmc/launcher/meta/Index_test.cpp new file mode 100644 index 0000000..5d3ddc5 --- /dev/null +++ b/ultimmc/launcher/meta/Index_test.cpp @@ -0,0 +1,37 @@ +#include +#include "TestUtil.h" + +#include "meta/Index.h" +#include "meta/VersionList.h" + +class IndexTest : public QObject +{ + Q_OBJECT +private +slots: + void test_hasUid_and_getList() + { + Meta::Index windex({std::make_shared("list1"), std::make_shared("list2"), std::make_shared("list3")}); + QVERIFY(windex.hasUid("list1")); + QVERIFY(!windex.hasUid("asdf")); + QVERIFY(windex.get("list2") != nullptr); + QCOMPARE(windex.get("list2")->uid(), QString("list2")); + QVERIFY(windex.get("adsf") != nullptr); + } + + void test_merge() + { + Meta::Index windex({std::make_shared("list1"), std::make_shared("list2"), std::make_shared("list3")}); + QCOMPARE(windex.lists().size(), 3); + windex.merge(std::shared_ptr(new Meta::Index({std::make_shared("list1"), std::make_shared("list2"), std::make_shared("list3")}))); + QCOMPARE(windex.lists().size(), 3); + windex.merge(std::shared_ptr(new Meta::Index({std::make_shared("list4"), std::make_shared("list2"), std::make_shared("list5")}))); + QCOMPARE(windex.lists().size(), 5); + windex.merge(std::shared_ptr(new Meta::Index({std::make_shared("list6")}))); + QCOMPARE(windex.lists().size(), 6); + } +}; + +QTEST_GUILESS_MAIN(IndexTest) + +#include "Index_test.moc" diff --git a/ultimmc/launcher/meta/JsonFormat.cpp b/ultimmc/launcher/meta/JsonFormat.cpp new file mode 100644 index 0000000..67a2cdf --- /dev/null +++ b/ultimmc/launcher/meta/JsonFormat.cpp @@ -0,0 +1,217 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "JsonFormat.h" + +// FIXME: remove this from here... somehow +#include "minecraft/OneSixVersionFormat.h" +#include "Json.h" + +#include "Index.h" +#include "Version.h" +#include "VersionList.h" + +using namespace Json; + +namespace Meta +{ + +MetadataVersion currentFormatVersion() +{ + return MetadataVersion::InitialRelease; +} + +// Index +static std::shared_ptr parseIndexInternal(const QJsonObject &obj) +{ + const QVector objects = requireIsArrayOf(obj, "packages"); + QVector lists; + lists.reserve(objects.size()); + std::transform(objects.begin(), objects.end(), std::back_inserter(lists), [](const QJsonObject &obj) + { + VersionListPtr list = std::make_shared(requireString(obj, "uid")); + list->setName(ensureString(obj, "name", QString())); + return list; + }); + return std::make_shared(lists); +} + +// Version +static VersionPtr parseCommonVersion(const QString &uid, const QJsonObject &obj) +{ + VersionPtr version = std::make_shared(uid, requireString(obj, "version")); + version->setTime(QDateTime::fromString(requireString(obj, "releaseTime"), Qt::ISODate).toMSecsSinceEpoch() / 1000); + version->setType(ensureString(obj, "type", QString())); + version->setRecommended(ensureBoolean(obj, QString("recommended"), false)); + version->setVolatile(ensureBoolean(obj, QString("volatile"), false)); + RequireSet depends, conflicts; + parseRequires(obj, &depends, "requires"); + parseRequires(obj, &conflicts, "conflicts"); + version->setRequires(depends, conflicts); + return version; +} + +static std::shared_ptr parseVersionInternal(const QJsonObject &obj) +{ + VersionPtr version = parseCommonVersion(requireString(obj, "uid"), obj); + + version->setData(OneSixVersionFormat::versionFileFromJson(QJsonDocument(obj), + QString("%1/%2.json").arg(version->uid(), version->version()), + obj.contains("order"))); + return version; +} + +// Version list / package +static std::shared_ptr parseVersionListInternal(const QJsonObject &obj) +{ + const QString uid = requireString(obj, "uid"); + + const QVector versionsRaw = requireIsArrayOf(obj, "versions"); + QVector versions; + versions.reserve(versionsRaw.size()); + std::transform(versionsRaw.begin(), versionsRaw.end(), std::back_inserter(versions), [uid](const QJsonObject &vObj) + { + auto version = parseCommonVersion(uid, vObj); + version->setProvidesRecommendations(); + return version; + }); + + VersionListPtr list = std::make_shared(uid); + list->setName(ensureString(obj, "name", QString())); + list->setVersions(versions); + return list; +} + + +MetadataVersion parseFormatVersion(const QJsonObject &obj, bool required) +{ + if (!obj.contains("formatVersion")) + { + if(required) + { + return MetadataVersion::Invalid; + } + return MetadataVersion::InitialRelease; + } + if (!obj.value("formatVersion").isDouble()) + { + return MetadataVersion::Invalid; + } + switch(obj.value("formatVersion").toInt()) + { + case 0: + case 1: + return MetadataVersion::InitialRelease; + default: + return MetadataVersion::Invalid; + } +} + +void serializeFormatVersion(QJsonObject& obj, Meta::MetadataVersion version) +{ + if(version == MetadataVersion::Invalid) + { + return; + } + obj.insert("formatVersion", int(version)); +} + +void parseIndex(const QJsonObject &obj, Index *ptr) +{ + const MetadataVersion version = parseFormatVersion(obj); + switch (version) + { + case MetadataVersion::InitialRelease: + ptr->merge(parseIndexInternal(obj)); + break; + case MetadataVersion::Invalid: + throw ParseException(QObject::tr("Unknown format version!")); + } +} + +void parseVersionList(const QJsonObject &obj, VersionList *ptr) +{ + const MetadataVersion version = parseFormatVersion(obj); + switch (version) + { + case MetadataVersion::InitialRelease: + ptr->merge(parseVersionListInternal(obj)); + break; + case MetadataVersion::Invalid: + throw ParseException(QObject::tr("Unknown format version!")); + } +} + +void parseVersion(const QJsonObject &obj, Version *ptr) +{ + const MetadataVersion version = parseFormatVersion(obj); + switch (version) + { + case MetadataVersion::InitialRelease: + ptr->merge(parseVersionInternal(obj)); + break; + case MetadataVersion::Invalid: + throw ParseException(QObject::tr("Unknown format version!")); + } +} + +/* +[ +{"uid":"foo", "equals":"version"} +] +*/ +void parseRequires(const QJsonObject& obj, RequireSet* ptr, const char * keyName) +{ + if(obj.contains(keyName)) + { + auto reqArray = requireArray(obj, keyName); + auto iter = reqArray.begin(); + while(iter != reqArray.end()) + { + auto reqObject = requireValueObject(*iter); + auto uid = requireString(reqObject, "uid"); + auto equals = ensureString(reqObject, "equals", QString()); + auto suggests = ensureString(reqObject, "suggests", QString()); + ptr->insert({uid, equals, suggests}); + iter++; + } + } +} +void serializeRequires(QJsonObject& obj, RequireSet* ptr, const char * keyName) +{ + if(!ptr || ptr->empty()) + { + return; + } + QJsonArray arrOut; + for(auto &iter: *ptr) + { + QJsonObject reqOut; + reqOut.insert("uid", iter.uid); + if(!iter.equalsVersion.isEmpty()) + { + reqOut.insert("equals", iter.equalsVersion); + } + if(!iter.suggests.isEmpty()) + { + reqOut.insert("suggests", iter.suggests); + } + arrOut.append(reqOut); + } + obj.insert(keyName, arrOut); +} + +} + diff --git a/ultimmc/launcher/meta/JsonFormat.h b/ultimmc/launcher/meta/JsonFormat.h new file mode 100644 index 0000000..93217b7 --- /dev/null +++ b/ultimmc/launcher/meta/JsonFormat.h @@ -0,0 +1,83 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "Exception.h" +#include "meta/BaseEntity.h" +#include + +namespace Meta +{ +class Index; +class Version; +class VersionList; + +enum class MetadataVersion +{ + Invalid = -1, + InitialRelease = 1 +}; + +class ParseException : public Exception +{ +public: + using Exception::Exception; +}; +struct Require +{ + bool operator==(const Require & rhs) const + { + return uid == rhs.uid; + } + bool operator<(const Require & rhs) const + { + return uid < rhs.uid; + } + bool deepEquals(const Require & rhs) const + { + return uid == rhs.uid + && equalsVersion == rhs.equalsVersion + && suggests == rhs.suggests; + } + QString uid; + QString equalsVersion; + QString suggests; +}; + +inline Q_DECL_PURE_FUNCTION uint qHash(const Require &key, uint seed = 0) Q_DECL_NOTHROW +{ + return qHash(key.uid, seed); +} + +using RequireSet = std::set; + +void parseIndex(const QJsonObject &obj, Index *ptr); +void parseVersion(const QJsonObject &obj, Version *ptr); +void parseVersionList(const QJsonObject &obj, VersionList *ptr); + +MetadataVersion parseFormatVersion(const QJsonObject &obj, bool required = true); +void serializeFormatVersion(QJsonObject &obj, MetadataVersion version); + +// FIXME: this has a different shape than the others...FIX IT!? +void parseRequires(const QJsonObject &obj, RequireSet * ptr, const char * keyName = "requires"); +void serializeRequires(QJsonObject & objOut, RequireSet* ptr, const char * keyName = "requires"); +MetadataVersion currentFormatVersion(); +} + +Q_DECLARE_METATYPE(std::set) diff --git a/ultimmc/launcher/meta/Version.cpp b/ultimmc/launcher/meta/Version.cpp new file mode 100644 index 0000000..73409a2 --- /dev/null +++ b/ultimmc/launcher/meta/Version.cpp @@ -0,0 +1,140 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Version.h" + +#include + +#include "JsonFormat.h" +#include "minecraft/PackProfile.h" + +Meta::Version::Version(const QString &uid, const QString &version) + : BaseVersion(), m_uid(uid), m_version(version) +{ +} + +Meta::Version::~Version() +{ +} + +QString Meta::Version::descriptor() +{ + return m_version; +} +QString Meta::Version::name() +{ + if(m_data) + return m_data->name; + return m_uid; +} +QString Meta::Version::typeString() const +{ + return m_type; +} + +QDateTime Meta::Version::time() const +{ + return QDateTime::fromMSecsSinceEpoch(m_time * 1000, Qt::UTC); +} + +void Meta::Version::parse(const QJsonObject& obj) +{ + parseVersion(obj, this); +} + +void Meta::Version::mergeFromList(const Meta::VersionPtr& other) +{ + if(other->m_providesRecommendations) + { + if(m_recommended != other->m_recommended) + { + setRecommended(other->m_recommended); + } + } + if (m_type != other->m_type) + { + setType(other->m_type); + } + if (m_time != other->m_time) + { + setTime(other->m_time); + } + if (m_requires != other->m_requires) + { + m_requires = other->m_requires; + } + if (m_conflicts != other->m_conflicts) + { + m_conflicts = other->m_conflicts; + } + if(m_volatile != other->m_volatile) + { + setVolatile(other->m_volatile); + } +} + +void Meta::Version::merge(const VersionPtr &other) +{ + mergeFromList(other); + if(other->m_data) + { + setData(other->m_data); + } +} + +QString Meta::Version::localFilename() const +{ + return m_uid + '/' + m_version + ".json"; +} + +void Meta::Version::setType(const QString &type) +{ + m_type = type; + emit typeChanged(); +} + +void Meta::Version::setTime(const qint64 time) +{ + m_time = time; + emit timeChanged(); +} + +void Meta::Version::setRequires(const Meta::RequireSet &depends, const Meta::RequireSet &conflicts) +{ + m_requires = depends; + m_conflicts = conflicts; + emit requiresChanged(); +} + +void Meta::Version::setVolatile(bool volatile_) +{ + m_volatile = volatile_; +} + + +void Meta::Version::setData(const VersionFilePtr &data) +{ + m_data = data; +} + +void Meta::Version::setProvidesRecommendations() +{ + m_providesRecommendations = true; +} + +void Meta::Version::setRecommended(bool recommended) +{ + m_recommended = recommended; +} diff --git a/ultimmc/launcher/meta/Version.h b/ultimmc/launcher/meta/Version.h new file mode 100644 index 0000000..9ab2321 --- /dev/null +++ b/ultimmc/launcher/meta/Version.h @@ -0,0 +1,116 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "BaseVersion.h" + +#include +#include +#include +#include + +#include "minecraft/VersionFile.h" + +#include "BaseEntity.h" + +#include "JsonFormat.h" + +namespace Meta +{ +using VersionPtr = std::shared_ptr; + +class Version : public QObject, public BaseVersion, public BaseEntity +{ + Q_OBJECT + +public: /* con/des */ + explicit Version(const QString &uid, const QString &version); + virtual ~Version(); + + QString descriptor() override; + QString name() override; + QString typeString() const override; + + QString uid() const + { + return m_uid; + } + QString version() const + { + return m_version; + } + QString type() const + { + return m_type; + } + QDateTime time() const; + qint64 rawTime() const + { + return m_time; + } + const Meta::RequireSet &depends() const + { + return m_requires; + } + VersionFilePtr data() const + { + return m_data; + } + bool isRecommended() const + { + return m_recommended; + } + bool isLoaded() const + { + return m_data != nullptr; + } + + void merge(const VersionPtr &other); + void mergeFromList(const VersionPtr &other); + void parse(const QJsonObject &obj) override; + + QString localFilename() const override; + +public: // for usage by format parsers only + void setType(const QString &type); + void setTime(const qint64 time); + void setRequires(const Meta::RequireSet &depends, const Meta::RequireSet &conflicts); + void setVolatile(bool volatile_); + void setRecommended(bool recommended); + void setProvidesRecommendations(); + void setData(const VersionFilePtr &data); + +signals: + void typeChanged(); + void timeChanged(); + void requiresChanged(); + +private: + bool m_providesRecommendations = false; + bool m_recommended = false; + QString m_name; + QString m_uid; + QString m_version; + QString m_type; + qint64 m_time = 0; + Meta::RequireSet m_requires; + Meta::RequireSet m_conflicts; + bool m_volatile = false; + VersionFilePtr m_data; +}; +} + +Q_DECLARE_METATYPE(Meta::VersionPtr) diff --git a/ultimmc/launcher/meta/VersionList.cpp b/ultimmc/launcher/meta/VersionList.cpp new file mode 100644 index 0000000..a0540c8 --- /dev/null +++ b/ultimmc/launcher/meta/VersionList.cpp @@ -0,0 +1,245 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "VersionList.h" + +#include + +#include "Version.h" +#include "JsonFormat.h" +#include "Version.h" + +namespace Meta +{ +VersionList::VersionList(const QString &uid, QObject *parent) + : BaseVersionList(parent), m_uid(uid) +{ + setObjectName("Version list: " + uid); +} + +Task::Ptr VersionList::getLoadTask() +{ + load(Net::Mode::Online); + return getCurrentTask(); +} + +bool VersionList::isLoaded() +{ + return BaseEntity::isLoaded(); +} + +const BaseVersionPtr VersionList::at(int i) const +{ + return m_versions.at(i); +} +int VersionList::count() const +{ + return m_versions.size(); +} + +void VersionList::sortVersions() +{ + beginResetModel(); + std::sort(m_versions.begin(), m_versions.end(), [](const VersionPtr &a, const VersionPtr &b) + { + return *a.get() < *b.get(); + }); + endResetModel(); +} + +QVariant VersionList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_versions.size() || index.parent().isValid()) + { + return QVariant(); + } + + VersionPtr version = m_versions.at(index.row()); + + switch (role) + { + case VersionPointerRole: return QVariant::fromValue(std::dynamic_pointer_cast(version)); + case VersionRole: + case VersionIdRole: + return version->version(); + case ParentVersionRole: + { + // FIXME: HACK: this should be generic and be replaced by something else. Anything that is a hard 'equals' dep is a 'parent uid'. + auto & reqs = version->depends(); + auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Require & req) + { + return req.uid == "net.minecraft"; + }); + if (iter != reqs.end()) + { + return (*iter).equalsVersion; + } + return QVariant(); + } + case TypeRole: return version->type(); + + case UidRole: return version->uid(); + case TimeRole: return version->time(); + case RequiresRole: return QVariant::fromValue(version->depends()); + case SortRole: return version->rawTime(); + case VersionPtrRole: return QVariant::fromValue(version); + case RecommendedRole: return version->isRecommended(); + // FIXME: this should be determined in whatever view/proxy is used... + // case LatestRole: return version == getLatestStable(); + default: return QVariant(); + } +} + +BaseVersionList::RoleList VersionList::providesRoles() const +{ + return {VersionPointerRole, VersionRole, VersionIdRole, ParentVersionRole, + TypeRole, UidRole, TimeRole, RequiresRole, SortRole, + RecommendedRole, LatestRole, VersionPtrRole}; +} + +QHash VersionList::roleNames() const +{ + QHash roles = BaseVersionList::roleNames(); + roles.insert(UidRole, "uid"); + roles.insert(TimeRole, "time"); + roles.insert(SortRole, "sort"); + roles.insert(RequiresRole, "requires"); + return roles; +} + +QString VersionList::localFilename() const +{ + return m_uid + "/index.json"; +} + +QString VersionList::humanReadable() const +{ + return m_name.isEmpty() ? m_uid : m_name; +} + +VersionPtr VersionList::getVersion(const QString &version) +{ + VersionPtr out = m_lookup.value(version, nullptr); + if(!out) + { + out = std::make_shared(m_uid, version); + m_lookup[version] = out; + } + return out; +} + +void VersionList::setName(const QString &name) +{ + m_name = name; + emit nameChanged(name); +} + +void VersionList::setVersions(const QVector &versions) +{ + beginResetModel(); + m_versions = versions; + std::sort(m_versions.begin(), m_versions.end(), [](const VersionPtr &a, const VersionPtr &b) + { + return a->rawTime() > b->rawTime(); + }); + for (int i = 0; i < m_versions.size(); ++i) + { + m_lookup.insert(m_versions.at(i)->version(), m_versions.at(i)); + setupAddedVersion(i, m_versions.at(i)); + } + + // FIXME: this is dumb, we have 'recommended' as part of the metadata already... + auto recommendedIt = std::find_if(m_versions.constBegin(), m_versions.constEnd(), [](const VersionPtr &ptr) { return ptr->type() == "release"; }); + m_recommended = recommendedIt == m_versions.constEnd() ? nullptr : *recommendedIt; + endResetModel(); +} + +void VersionList::parse(const QJsonObject& obj) +{ + parseVersionList(obj, this); +} + +// FIXME: this is dumb, we have 'recommended' as part of the metadata already... +static const Meta::VersionPtr &getBetterVersion(const Meta::VersionPtr &a, const Meta::VersionPtr &b) +{ + if(!a) + return b; + if(!b) + return a; + if(a->type() == b->type()) + { + // newer of same type wins + return (a->rawTime() > b->rawTime() ? a : b); + } + // 'release' type wins + return (a->type() == "release" ? a : b); +} + +void VersionList::mergeFromIndex(const VersionListPtr &other) +{ + if (m_name != other->m_name) + { + setName(other->m_name); + } +} + +void VersionList::merge(const VersionListPtr &other) +{ + if (m_name != other->m_name) + { + setName(other->m_name); + } + + // TODO: do not reset the whole model. maybe? + beginResetModel(); + m_versions.clear(); + if(other->m_versions.isEmpty()) + { + qWarning() << "Empty list loaded ..."; + } + for (const VersionPtr &version : other->m_versions) + { + // we already have the version. merge the contents + if (m_lookup.contains(version->version())) + { + m_lookup.value(version->version())->mergeFromList(version); + } + else + { + m_lookup.insert(version->uid(), version); + } + // connect it. + setupAddedVersion(m_versions.size(), version); + m_versions.append(version); + m_recommended = getBetterVersion(m_recommended, version); + } + endResetModel(); +} + +void VersionList::setupAddedVersion(const int row, const VersionPtr &version) +{ + // FIXME: do not disconnect from everythin, disconnect only the lambdas here + version->disconnect(); + connect(version.get(), &Version::requiresChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector() << RequiresRole); }); + connect(version.get(), &Version::timeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector() << TimeRole << SortRole); }); + connect(version.get(), &Version::typeChanged, this, [this, row]() { emit dataChanged(index(row), index(row), QVector() << TypeRole); }); +} + +BaseVersionPtr VersionList::getRecommended() const +{ + return m_recommended; +} + +} diff --git a/ultimmc/launcher/meta/VersionList.h b/ultimmc/launcher/meta/VersionList.h new file mode 100644 index 0000000..378255d --- /dev/null +++ b/ultimmc/launcher/meta/VersionList.h @@ -0,0 +1,101 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "BaseEntity.h" +#include "BaseVersionList.h" +#include +#include + +namespace Meta +{ +using VersionPtr = std::shared_ptr; +using VersionListPtr = std::shared_ptr; + +class VersionList : public BaseVersionList, public BaseEntity +{ + Q_OBJECT + Q_PROPERTY(QString uid READ uid CONSTANT) + Q_PROPERTY(QString name READ name NOTIFY nameChanged) +public: + explicit VersionList(const QString &uid, QObject *parent = nullptr); + + enum Roles + { + UidRole = Qt::UserRole + 100, + TimeRole, + RequiresRole, + VersionPtrRole + }; + + Task::Ptr getLoadTask() override; + bool isLoaded() override; + const BaseVersionPtr at(int i) const override; + int count() const override; + void sortVersions() override; + + BaseVersionPtr getRecommended() const override; + + QVariant data(const QModelIndex &index, int role) const override; + RoleList providesRoles() const override; + QHash roleNames() const override; + + QString localFilename() const override; + + QString uid() const + { + return m_uid; + } + QString name() const + { + return m_name; + } + QString humanReadable() const; + + VersionPtr getVersion(const QString &version); + + QVector versions() const + { + return m_versions; + } + +public: // for usage only by parsers + void setName(const QString &name); + void setVersions(const QVector &versions); + void merge(const VersionListPtr &other); + void mergeFromIndex(const VersionListPtr &other); + void parse(const QJsonObject &obj) override; + +signals: + void nameChanged(const QString &name); + +protected slots: + void updateListData(QList) override + { + } + +private: + QVector m_versions; + QHash m_lookup; + QString m_uid; + QString m_name; + + VersionPtr m_recommended; + + void setupAddedVersion(const int row, const VersionPtr &version); +}; +} +Q_DECLARE_METATYPE(Meta::VersionListPtr) diff --git a/ultimmc/launcher/minecraft/AssetsUtils.cpp b/ultimmc/launcher/minecraft/AssetsUtils.cpp new file mode 100644 index 0000000..7290aeb --- /dev/null +++ b/ultimmc/launcher/minecraft/AssetsUtils.cpp @@ -0,0 +1,335 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "AssetsUtils.h" +#include "FileSystem.h" +#include "net/Download.h" +#include "net/ChecksumValidator.h" +#include "BuildConfig.h" + +#include "Application.h" + +namespace { +QSet collectPathsFromDir(QString dirPath) +{ + QFileInfo dirInfo(dirPath); + + if (!dirInfo.exists()) + { + return {}; + } + + QSet out; + + QDirIterator iter(dirPath, QDirIterator::Subdirectories); + while (iter.hasNext()) + { + QString value = iter.next(); + QFileInfo info(value); + if(info.isFile()) + { + out.insert(value); + qDebug() << value; + } + } + return out; +} +} + + +namespace AssetsUtils +{ + +/* + * Returns true on success, with index populated + * index is undefined otherwise + */ +bool loadAssetsIndexJson(const QString &assetsId, const QString &path, AssetsIndex& index) +{ + /* + { + "objects": { + "icons/icon_16x16.png": { + "hash": "bdf48ef6b5d0d23bbb02e17d04865216179f510a", + "size": 3665 + }, + ... + } + } + } + */ + + QFile file(path); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << "Failed to read assets index file" << path; + return false; + } + index.id = assetsId; + + // Read the file and close it. + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); + + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) + { + qCritical() << "Failed to parse assets index file:" << parseError.errorString() + << "at offset " << QString::number(parseError.offset); + return false; + } + + // Make sure the root is an object. + if (!jsonDoc.isObject()) + { + qCritical() << "Invalid assets index JSON: Root should be an array."; + return false; + } + + QJsonObject root = jsonDoc.object(); + + QJsonValue isVirtual = root.value("virtual"); + if (!isVirtual.isUndefined()) + { + index.isVirtual = isVirtual.toBool(false); + } + + QJsonValue mapToResources = root.value("map_to_resources"); + if (!mapToResources.isUndefined()) + { + index.mapToResources = mapToResources.toBool(false); + } + + QJsonValue objects = root.value("objects"); + QVariantMap map = objects.toVariant().toMap(); + + for (QVariantMap::const_iterator iter = map.begin(); iter != map.end(); ++iter) + { + // qDebug() << iter.key(); + + QVariant variant = iter.value(); + QVariantMap nested_objects = variant.toMap(); + + AssetObject object; + + for (QVariantMap::const_iterator nested_iter = nested_objects.begin(); + nested_iter != nested_objects.end(); ++nested_iter) + { + // qDebug() << nested_iter.key() << nested_iter.value().toString(); + QString key = nested_iter.key(); + QVariant value = nested_iter.value(); + + if (key == "hash") + { + object.hash = value.toString(); + } + else if (key == "size") + { + object.size = value.toDouble(); + } + } + + index.objects.insert(iter.key(), object); + } + + return true; +} + +// FIXME: ugly code duplication +QDir getAssetsDir(const QString &assetsId, const QString &resourcesFolder) +{ + QDir assetsDir = QDir("assets/"); + QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes")); + QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects")); + QDir virtualDir = QDir(FS::PathCombine(assetsDir.path(), "virtual")); + + QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json"); + QFile indexFile(indexPath); + QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId)); + + if (!indexFile.exists()) + { + qCritical() << "No assets index file" << indexPath << "; can't determine assets path!"; + return virtualRoot; + } + + AssetsIndex index; + if(!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index)) + { + qCritical() << "Failed to load asset index file" << indexPath << "; can't determine assets path!"; + return virtualRoot; + } + + QString targetPath; + if(index.isVirtual) + { + return virtualRoot; + } + else if(index.mapToResources) + { + return QDir(resourcesFolder); + } + return virtualRoot; +} + +// FIXME: ugly code duplication +bool reconstructAssets(QString assetsId, QString resourcesFolder) +{ + QDir assetsDir = QDir("assets/"); + QDir indexDir = QDir(FS::PathCombine(assetsDir.path(), "indexes")); + QDir objectDir = QDir(FS::PathCombine(assetsDir.path(), "objects")); + QDir virtualDir = QDir(FS::PathCombine(assetsDir.path(), "virtual")); + + QString indexPath = FS::PathCombine(indexDir.path(), assetsId + ".json"); + QFile indexFile(indexPath); + QDir virtualRoot(FS::PathCombine(virtualDir.path(), assetsId)); + + if (!indexFile.exists()) + { + qCritical() << "No assets index file" << indexPath << "; can't reconstruct assets!"; + return false; + } + + qDebug() << "reconstructAssets" << assetsDir.path() << indexDir.path() << objectDir.path() << virtualDir.path() << virtualRoot.path(); + + AssetsIndex index; + if(!AssetsUtils::loadAssetsIndexJson(assetsId, indexPath, index)) + { + qCritical() << "Failed to load asset index file" << indexPath << "; can't reconstruct assets!"; + return false; + } + + QString targetPath; + bool removeLeftovers = false; + if(index.isVirtual) + { + targetPath = virtualRoot.path(); + removeLeftovers = true; + qDebug() << "Reconstructing virtual assets folder at" << targetPath; + } + else if(index.mapToResources) + { + targetPath = resourcesFolder; + qDebug() << "Reconstructing resources folder at" << targetPath; + } + + if (!targetPath.isNull()) + { + auto presentFiles = collectPathsFromDir(targetPath); + for (QString map : index.objects.keys()) + { + AssetObject asset_object = index.objects.value(map); + QString target_path = FS::PathCombine(targetPath, map); + QFile target(target_path); + + QString tlk = asset_object.hash.left(2); + + QString original_path = FS::PathCombine(objectDir.path(), tlk, asset_object.hash); + QFile original(original_path); + if (!original.exists()) + continue; + + presentFiles.remove(target_path); + + if (!target.exists()) + { + QFileInfo info(target_path); + QDir target_dir = info.dir(); + + qDebug() << target_dir.path(); + FS::ensureFolderPathExists(target_dir.path()); + + bool couldCopy = original.copy(target_path); + qDebug() << " Copying" << original_path << "to" << target_path << QString::number(couldCopy); + } + } + + // TODO: Write last used time to virtualRoot/.lastused + if(removeLeftovers) + { + for(auto & file: presentFiles) + { + qDebug() << "Would remove" << file; + } + } + } + return true; +} + +} + +NetAction::Ptr AssetObject::getDownloadAction() +{ + QFileInfo objectFile(getLocalPath()); + if ((!objectFile.isFile()) || (objectFile.size() != size)) + { + auto objectDL = Net::Download::makeFile(getUrl(), objectFile.filePath()); + if(hash.size()) + { + auto rawHash = QByteArray::fromHex(hash.toLatin1()); + objectDL->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); + } + objectDL->m_total_progress = size; + return objectDL; + } + return nullptr; +} + +QString AssetObject::getLocalPath() +{ + return "assets/objects/" + getRelPath(); +} + +QUrl AssetObject::getUrl() +{ + return BuildConfig.RESOURCE_BASE + getRelPath(); +} + +QString AssetObject::getRelPath() +{ + return hash.left(2) + "/" + hash; +} + +NetJob::Ptr AssetsIndex::getDownloadJob() +{ + auto job = new NetJob(QObject::tr("Assets for %1").arg(id), APPLICATION->network()); + for (auto &object : objects.values()) + { + auto dl = object.getDownloadAction(); + if(dl) + { + job->addNetAction(dl); + } + } + if(job->size()) + return job; + return nullptr; +} diff --git a/ultimmc/launcher/minecraft/AssetsUtils.h b/ultimmc/launcher/minecraft/AssetsUtils.h new file mode 100644 index 0000000..3dbf19e --- /dev/null +++ b/ultimmc/launcher/minecraft/AssetsUtils.h @@ -0,0 +1,53 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include "net/NetAction.h" +#include "net/NetJob.h" + +struct AssetObject +{ + QString getRelPath(); + QUrl getUrl(); + QString getLocalPath(); + NetAction::Ptr getDownloadAction(); + + QString hash; + qint64 size; +}; + +struct AssetsIndex +{ + NetJob::Ptr getDownloadJob(); + + QString id; + QMap objects; + bool isVirtual = false; + bool mapToResources = false; +}; + +/// FIXME: this is absolutely horrendous. REDO!!!! +namespace AssetsUtils +{ +bool loadAssetsIndexJson(const QString &id, const QString &file, AssetsIndex& index); + +QDir getAssetsDir(const QString &assetsId, const QString &resourcesFolder); + +/// Reconstruct a virtual assets folder for the given assets ID and return the folder +bool reconstructAssets(QString assetsId, QString resourcesFolder); +} diff --git a/ultimmc/launcher/minecraft/Component.cpp b/ultimmc/launcher/minecraft/Component.cpp new file mode 100644 index 0000000..1926302 --- /dev/null +++ b/ultimmc/launcher/minecraft/Component.cpp @@ -0,0 +1,441 @@ +#include +#include +#include "Component.h" + +#include + +#include "meta/Version.h" +#include "VersionFile.h" +#include "minecraft/PackProfile.h" +#include "FileSystem.h" +#include "OneSixVersionFormat.h" +#include "Application.h" + +#include + +Component::Component(PackProfile * parent, const QString& uid) +{ + assert(parent); + m_parent = parent; + + m_uid = uid; +} + +Component::Component(PackProfile * parent, std::shared_ptr version) +{ + assert(parent); + m_parent = parent; + + m_metaVersion = version; + m_uid = version->uid(); + m_version = m_cachedVersion = version->version(); + m_cachedName = version->name(); + m_loaded = version->isLoaded(); +} + +Component::Component(PackProfile * parent, const QString& uid, std::shared_ptr file) +{ + assert(parent); + m_parent = parent; + + m_file = file; + m_uid = uid; + m_cachedVersion = m_file->version; + m_cachedName = m_file->name; + m_loaded = true; +} + +std::shared_ptr Component::getMeta() +{ + return m_metaVersion; +} + +void Component::applyTo(LaunchProfile* profile) +{ + // do not apply disabled components + if(!isEnabled()) + { + return; + } + auto vfile = getVersionFile(); + if(vfile) + { + vfile->applyTo(profile); + } + else + { + profile->applyProblemSeverity(getProblemSeverity()); + } +} + +std::shared_ptr Component::getVersionFile() const +{ + if(m_metaVersion) + { + if(!m_metaVersion->isLoaded()) + { + m_metaVersion->load(Net::Mode::Online); + } + return m_metaVersion->data(); + } + else + { + return m_file; + } +} + +std::shared_ptr Component::getVersionList() const +{ + // FIXME: what if the metadata index isn't loaded yet? + if(APPLICATION->metadataIndex()->hasUid(m_uid)) + { + return APPLICATION->metadataIndex()->get(m_uid); + } + return nullptr; +} + +int Component::getOrder() +{ + if(m_orderOverride) + return m_order; + + auto vfile = getVersionFile(); + if(vfile) + { + return vfile->order; + } + return 0; +} +void Component::setOrder(int order) +{ + m_orderOverride = true; + m_order = order; +} +QString Component::getID() +{ + return m_uid; +} +QString Component::getName() +{ + if (!m_cachedName.isEmpty()) + return m_cachedName; + return m_uid; +} +QString Component::getVersion() +{ + return m_cachedVersion; +} +QString Component::getFilename() +{ + return m_parent->patchFilePathForUid(m_uid); +} +QDateTime Component::getReleaseDateTime() +{ + if(m_metaVersion) + { + return m_metaVersion->time(); + } + auto vfile = getVersionFile(); + if(vfile) + { + return vfile->releaseTime; + } + // FIXME: fake + return QDateTime::currentDateTime(); +} + +bool Component::isEnabled() +{ + return !canBeDisabled() || !m_disabled; +} + +bool Component::canBeDisabled() +{ + return isRemovable() && !m_dependencyOnly; +} + +bool Component::setEnabled(bool state) +{ + bool intendedDisabled = !state; + if (!canBeDisabled()) + { + intendedDisabled = false; + } + if(intendedDisabled != m_disabled) + { + m_disabled = intendedDisabled; + emit dataChanged(); + return true; + } + return false; +} + +bool Component::isCustom() +{ + return m_file != nullptr; +} + +bool Component::isCustomizable() +{ + if(m_metaVersion) + { + if(getVersionFile()) + { + return true; + } + } + return false; +} +bool Component::isRemovable() +{ + return !m_important; +} +bool Component::isRevertible() +{ + if (isCustom()) + { + if(APPLICATION->metadataIndex()->hasUid(m_uid)) + { + return true; + } + } + return false; +} +bool Component::isMoveable() +{ + // HACK, FIXME: this was too dumb and wouldn't follow dependency constraints anyway. For now hardcoded to 'true'. + return true; +} +bool Component::isVersionChangeable() +{ + auto list = getVersionList(); + if(list) + { + if(!list->isLoaded()) + { + list->load(Net::Mode::Online); + } + return list->count() != 0; + } + return false; +} + +void Component::setImportant(bool state) +{ + if(m_important != state) + { + m_important = state; + emit dataChanged(); + } +} + +ProblemSeverity Component::getProblemSeverity() const +{ + auto file = getVersionFile(); + if(file) + { + return file->getProblemSeverity(); + } + return ProblemSeverity::Error; +} + +const QList Component::getProblems() const +{ + auto file = getVersionFile(); + if(file) + { + return file->getProblems(); + } + return {{ProblemSeverity::Error, QObject::tr("Patch is not loaded yet.")}}; +} + +void Component::setVersion(const QString& version) +{ + if(version == m_version) + { + return; + } + m_version = version; + if(m_loaded) + { + // we are loaded and potentially have state to invalidate + if(m_file) + { + // we have a file... explicit version has been changed and there is nothing else to do. + } + else + { + // we don't have a file, therefore we are loaded with metadata + m_cachedVersion = version; + // see if the meta version is loaded + auto metaVersion = APPLICATION->metadataIndex()->get(m_uid, version); + if(metaVersion->isLoaded()) + { + // if yes, we can continue with that. + m_metaVersion = metaVersion; + } + else + { + // if not, we need loading + m_metaVersion.reset(); + m_loaded = false; + } + updateCachedData(); + } + } + else + { + // not loaded... assume it will be sorted out later by the update task + } + emit dataChanged(); +} + +bool Component::customize() +{ + if(isCustom()) + { + return false; + } + + auto filename = getFilename(); + if(!FS::ensureFilePathExists(filename)) + { + return false; + } + // FIXME: get rid of this try-catch. + try + { + QSaveFile jsonFile(filename); + if(!jsonFile.open(QIODevice::WriteOnly)) + { + return false; + } + auto vfile = getVersionFile(); + if(!vfile) + { + return false; + } + auto document = OneSixVersionFormat::versionFileToJson(vfile); + jsonFile.write(document.toJson()); + if(!jsonFile.commit()) + { + return false; + } + m_file = vfile; + m_metaVersion.reset(); + emit dataChanged(); + } + catch (const Exception &error) + { + qWarning() << "Version could not be loaded:" << error.cause(); + } + return true; +} + +bool Component::revert() +{ + if(!isCustom()) + { + // already not custom + return true; + } + auto filename = getFilename(); + bool result = true; + // just kill the file and reload + if(QFile::exists(filename)) + { + result = QFile::remove(filename); + } + if(result) + { + // file gone... + m_file.reset(); + + // check local cache for metadata... + auto version = APPLICATION->metadataIndex()->get(m_uid, m_version); + if(version->isLoaded()) + { + m_metaVersion = version; + } + else + { + m_metaVersion.reset(); + m_loaded = false; + } + emit dataChanged(); + } + return result; +} + +/** + * deep inspecting compare for requirement sets + * By default, only uids are compared for set operations. + * This compares all fields of the Require structs in the sets. + */ +static bool deepCompare(const std::set & a, const std::set & b) +{ + // NOTE: this needs to be rewritten if the type of Meta::RequireSet changes + if(a.size() != b.size()) + { + return false; + } + for(const auto & reqA :a) + { + const auto &iter2 = b.find(reqA); + if(iter2 == b.cend()) + { + return false; + } + const auto & reqB = *iter2; + if(!reqA.deepEquals(reqB)) + { + return false; + } + } + return true; +} + +void Component::updateCachedData() +{ + auto file = getVersionFile(); + if(file) + { + bool changed = false; + if(m_cachedName != file->name) + { + m_cachedName = file->name; + changed = true; + } + if(m_cachedVersion != file->version) + { + m_cachedVersion = file->version; + changed = true; + } + if(m_cachedVolatile != file->m_volatile) + { + m_cachedVolatile = file->m_volatile; + changed = true; + } + if(!deepCompare(m_cachedRequires, file->depends)) + { + m_cachedRequires = file->depends; + changed = true; + } + if(!deepCompare(m_cachedConflicts, file->conflicts)) + { + m_cachedConflicts = file->conflicts; + changed = true; + } + if(changed) + { + emit dataChanged(); + } + } + else + { + // in case we removed all the metadata + m_cachedRequires.clear(); + m_cachedConflicts.clear(); + emit dataChanged(); + } +} diff --git a/ultimmc/launcher/minecraft/Component.h b/ultimmc/launcher/minecraft/Component.h new file mode 100644 index 0000000..ef7c994 --- /dev/null +++ b/ultimmc/launcher/minecraft/Component.h @@ -0,0 +1,110 @@ +#pragma once + +#include +#include +#include +#include +#include "meta/JsonFormat.h" +#include "ProblemProvider.h" +#include "QObjectPtr.h" + +class PackProfile; +class LaunchProfile; +namespace Meta +{ + class Version; + class VersionList; +} +class VersionFile; + +class Component : public QObject, public ProblemProvider +{ +Q_OBJECT +public: + Component(PackProfile * parent, const QString &uid); + + // DEPRECATED: remove these constructors? + Component(PackProfile * parent, std::shared_ptr version); + Component(PackProfile * parent, const QString & uid, std::shared_ptr file); + + virtual ~Component(){}; + void applyTo(LaunchProfile *profile); + + bool isEnabled(); + bool setEnabled (bool state); + bool canBeDisabled(); + + bool isMoveable(); + bool isCustomizable(); + bool isRevertible(); + bool isRemovable(); + bool isCustom(); + bool isVersionChangeable(); + + // DEPRECATED: explicit numeric order values, used for loading old non-component config. TODO: refactor and move to migration code + void setOrder(int order); + int getOrder(); + + QString getID(); + QString getName(); + QString getVersion(); + std::shared_ptr getMeta(); + QDateTime getReleaseDateTime(); + + QString getFilename(); + + std::shared_ptr getVersionFile() const; + std::shared_ptr getVersionList() const; + + void setImportant (bool state); + + + const QList getProblems() const override; + ProblemSeverity getProblemSeverity() const override; + + void setVersion(const QString & version); + bool customize(); + bool revert(); + + void updateCachedData(); + +signals: + void dataChanged(); + +public: /* data */ + PackProfile * m_parent; + + // BEGIN: persistent component list properties + /// ID of the component + QString m_uid; + /// version of the component - when there's a custom json override, this is also the version the component reverts to + QString m_version; + /// if true, this has been added automatically to satisfy dependencies and may be automatically removed + bool m_dependencyOnly = false; + /// if true, the component is either the main component of the instance, or otherwise important and cannot be removed. + bool m_important = false; + /// if true, the component is disabled + bool m_disabled = false; + + /// cached name for display purposes, taken from the version file (meta or local override) + QString m_cachedName; + /// cached version for display AND other purposes, taken from the version file (meta or local override) + QString m_cachedVersion; + /// cached set of requirements, taken from the version file (meta or local override) + Meta::RequireSet m_cachedRequires; + Meta::RequireSet m_cachedConflicts; + /// if true, the component is volatile and may be automatically removed when no longer needed + bool m_cachedVolatile = false; + // END: persistent component list properties + + // DEPRECATED: explicit numeric order values, used for loading old non-component config. TODO: refactor and move to migration code + bool m_orderOverride = false; + int m_order = 0; + + // load state + std::shared_ptr m_metaVersion; + std::shared_ptr m_file; + bool m_loaded = false; +}; + +typedef shared_qobject_ptr ComponentPtr; diff --git a/ultimmc/launcher/minecraft/ComponentUpdateTask.cpp b/ultimmc/launcher/minecraft/ComponentUpdateTask.cpp new file mode 100644 index 0000000..ff7ed0a --- /dev/null +++ b/ultimmc/launcher/minecraft/ComponentUpdateTask.cpp @@ -0,0 +1,705 @@ +#include "ComponentUpdateTask.h" + +#include "PackProfile_p.h" +#include "PackProfile.h" +#include "Component.h" +#include "meta/Index.h" +#include "meta/VersionList.h" +#include "meta/Version.h" +#include "ComponentUpdateTask_p.h" +#include "cassert" +#include "Version.h" +#include "net/Mode.h" +#include "OneSixVersionFormat.h" + +#include "Application.h" + +/* + * This is responsible for loading the components of a component list AND resolving dependency issues between them + */ + +/* + * FIXME: the 'one shot async task' nature of this does not fit the intended usage + * Really, it should be a reactor/state machine that receives input from the application + * and dynamically adapts to changing requirements... + * + * The reactor should be the only entry into manipulating the PackProfile. + * See: https://en.wikipedia.org/wiki/Reactor_pattern + */ + +/* + * Or make this operate on a snapshot of the PackProfile state, then merge results in as long as the snapshot and PackProfile didn't change? + * If the component list changes, start over. + */ + +ComponentUpdateTask::ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile* list, QObject* parent) + : Task(parent) +{ + d.reset(new ComponentUpdateTaskData); + d->m_list = list; + d->mode = mode; + d->netmode = netmode; +} + +ComponentUpdateTask::~ComponentUpdateTask() +{ +} + +void ComponentUpdateTask::executeTask() +{ + qDebug() << "Loading components"; + loadComponents(); +} + +namespace +{ +enum class LoadResult +{ + LoadedLocal, + RequiresRemote, + Failed +}; + +LoadResult composeLoadResult(LoadResult a, LoadResult b) +{ + if (a < b) + { + return b; + } + return a; +} + +static LoadResult loadComponent(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode) +{ + if(component->m_loaded) + { + qDebug() << component->getName() << "is already loaded"; + return LoadResult::LoadedLocal; + } + + LoadResult result = LoadResult::Failed; + auto customPatchFilename = component->getFilename(); + if(QFile::exists(customPatchFilename)) + { + // if local file exists... + + // check for uid problems inside... + bool fileChanged = false; + auto file = ProfileUtils::parseJsonFile(QFileInfo(customPatchFilename), false); + if(file->uid != component->m_uid) + { + file->uid = component->m_uid; + fileChanged = true; + } + if(fileChanged) + { + // FIXME: @QUALITY do not ignore return value + ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), customPatchFilename); + } + + component->m_file = file; + component->m_loaded = true; + result = LoadResult::LoadedLocal; + } + else + { + auto metaVersion = APPLICATION->metadataIndex()->get(component->m_uid, component->m_version); + component->m_metaVersion = metaVersion; + if(metaVersion->isLoaded()) + { + component->m_loaded = true; + result = LoadResult::LoadedLocal; + } + else + { + metaVersion->load(netmode); + loadTask = metaVersion->getCurrentTask(); + if(loadTask) + result = LoadResult::RequiresRemote; + else if (metaVersion->isLoaded()) + result = LoadResult::LoadedLocal; + else + result = LoadResult::Failed; + } + } + return result; +} + +// FIXME: dead code. determine if this can still be useful? +/* +static LoadResult loadPackProfile(ComponentPtr component, Task::Ptr& loadTask, Net::Mode netmode) +{ + if(component->m_loaded) + { + qDebug() << component->getName() << "is already loaded"; + return LoadResult::LoadedLocal; + } + + LoadResult result = LoadResult::Failed; + auto metaList = APPLICATION->metadataIndex()->get(component->m_uid); + if(metaList->isLoaded()) + { + component->m_loaded = true; + result = LoadResult::LoadedLocal; + } + else + { + metaList->load(netmode); + loadTask = metaList->getCurrentTask(); + result = LoadResult::RequiresRemote; + } + return result; +} +*/ + +static LoadResult loadIndex(Task::Ptr& loadTask, Net::Mode netmode) +{ + // FIXME: DECIDE. do we want to run the update task anyway? + if(APPLICATION->metadataIndex()->isLoaded()) + { + qDebug() << "Index is already loaded"; + return LoadResult::LoadedLocal; + } + APPLICATION->metadataIndex()->load(netmode); + loadTask = APPLICATION->metadataIndex()->getCurrentTask(); + if(loadTask) + { + return LoadResult::RequiresRemote; + } + // FIXME: this is assuming the load succeeded... did it really? + return LoadResult::LoadedLocal; +} +} + +void ComponentUpdateTask::loadComponents() +{ + LoadResult result = LoadResult::LoadedLocal; + size_t taskIndex = 0; + size_t componentIndex = 0; + d->remoteLoadSuccessful = true; + // load the main index (it is needed to determine if components can revert) + { + // FIXME: tear out as a method? or lambda? + Task::Ptr indexLoadTask; + auto singleResult = loadIndex(indexLoadTask, d->netmode); + result = composeLoadResult(result, singleResult); + if(indexLoadTask) + { + qDebug() << "Remote loading is being run for metadata index"; + RemoteLoadStatus status; + status.type = RemoteLoadStatus::Type::Index; + d->remoteLoadStatusList.append(status); + connect(indexLoadTask.get(), &Task::succeeded, [=]() + { + remoteLoadSucceeded(taskIndex); + }); + connect(indexLoadTask.get(), &Task::failed, [=](const QString & error) + { + remoteLoadFailed(taskIndex, error); + }); + taskIndex++; + } + } + // load all the components OR their lists... + for (auto component: d->m_list->d->components) + { + Task::Ptr loadTask; + LoadResult singleResult; + RemoteLoadStatus::Type loadType; + // FIXME: to do this right, we need to load the lists and decide on which versions to use during dependency resolution. For now, ignore all that... +#if 0 + switch(d->mode) + { + case Mode::Launch: + { + singleResult = loadComponent(component, loadTask, d->netmode); + loadType = RemoteLoadStatus::Type::Version; + break; + } + case Mode::Resolution: + { + singleResult = loadPackProfile(component, loadTask, d->netmode); + loadType = RemoteLoadStatus::Type::List; + break; + } + } +#else + singleResult = loadComponent(component, loadTask, d->netmode); + loadType = RemoteLoadStatus::Type::Version; +#endif + if(singleResult == LoadResult::LoadedLocal) + { + component->updateCachedData(); + } + result = composeLoadResult(result, singleResult); + if (loadTask) + { + qDebug() << "Remote loading is being run for" << component->getName(); + connect(loadTask.get(), &Task::succeeded, [=]() + { + remoteLoadSucceeded(taskIndex); + }); + connect(loadTask.get(), &Task::failed, [=](const QString & error) + { + remoteLoadFailed(taskIndex, error); + }); + RemoteLoadStatus status; + status.type = loadType; + status.PackProfileIndex = componentIndex; + d->remoteLoadStatusList.append(status); + taskIndex++; + } + componentIndex++; + } + d->remoteTasksInProgress = taskIndex; + switch(result) + { + case LoadResult::LoadedLocal: + { + // Everything got loaded. Advance to dependency resolution. + resolveDependencies(d->mode == Mode::Launch || d->netmode == Net::Mode::Offline); + break; + } + case LoadResult::RequiresRemote: + { + // we wait for signals. + break; + } + case LoadResult::Failed: + { + emitFailed(tr("Some component metadata load tasks failed.")); + break; + } + } +} + +namespace +{ + struct RequireEx : public Meta::Require + { + size_t indexOfFirstDependee = 0; + }; + struct RequireCompositionResult + { + bool ok; + RequireEx outcome; + }; + using RequireExSet = std::set; +} + +static RequireCompositionResult composeRequirement(const RequireEx & a, const RequireEx & b) +{ + assert(a.uid == b.uid); + RequireEx out; + out.uid = a.uid; + out.indexOfFirstDependee = std::min(a.indexOfFirstDependee, b.indexOfFirstDependee); + if(a.equalsVersion.isEmpty()) + { + out.equalsVersion = b.equalsVersion; + } + else if (b.equalsVersion.isEmpty()) + { + out.equalsVersion = a.equalsVersion; + } + else if (a.equalsVersion == b.equalsVersion) + { + out.equalsVersion = a.equalsVersion; + } + else + { + // FIXME: mark error as explicit version conflict + return {false, out}; + } + + if(a.suggests.isEmpty()) + { + out.suggests = b.suggests; + } + else if (b.suggests.isEmpty()) + { + out.suggests = a.suggests; + } + else + { + Version aVer(a.suggests); + Version bVer(b.suggests); + out.suggests = (aVer < bVer ? b.suggests : a.suggests); + } + return {true, out}; +} + +// gather the requirements from all components, finding any obvious conflicts +static bool gatherRequirementsFromComponents(const ComponentContainer & input, RequireExSet & output) +{ + bool succeeded = true; + size_t componentNum = 0; + for(auto component: input) + { + auto &componentRequires = component->m_cachedRequires; + for(const auto & componentRequire: componentRequires) + { + auto found = std::find_if(output.cbegin(), output.cend(), [componentRequire](const Meta::Require & req){ + return req.uid == componentRequire.uid; + }); + + RequireEx componenRequireEx; + componenRequireEx.uid = componentRequire.uid; + componenRequireEx.suggests = componentRequire.suggests; + componenRequireEx.equalsVersion = componentRequire.equalsVersion; + componenRequireEx.indexOfFirstDependee = componentNum; + + if(found != output.cend()) + { + // found... process it further + auto result = composeRequirement(componenRequireEx, *found); + if(result.ok) + { + output.erase(componenRequireEx); + output.insert(result.outcome); + } + else + { + qCritical() + << "Conflicting requirements:" + << componentRequire.uid + << "versions:" + << componentRequire.equalsVersion + << ";" + << (*found).equalsVersion; + } + succeeded &= result.ok; + } + else + { + // not found, accumulate + output.insert(componenRequireEx); + } + } + componentNum++; + } + return succeeded; +} + +/// Get list of uids that can be trivially removed because nothing is depending on them anymore (and they are installed as deps) +static void getTrivialRemovals(const ComponentContainer & components, const RequireExSet & reqs, QStringList &toRemove) +{ + for(const auto & component: components) + { + if(!component->m_dependencyOnly) + continue; + if(!component->m_cachedVolatile) + continue; + RequireEx reqNeedle; + reqNeedle.uid = component->m_uid; + const auto iter = reqs.find(reqNeedle); + if(iter == reqs.cend()) + { + toRemove.append(component->m_uid); + } + } +} + +/** + * handles: + * - trivial addition (there is an unmet requirement and it can be trivially met by adding something) + * - trivial version conflict of dependencies == explicit version required and installed is different + * + * toAdd - set of requirements than mean adding a new component + * toChange - set of requirements that mean changing version of an existing component + */ +static bool getTrivialComponentChanges(const ComponentIndex & index, const RequireExSet & input, RequireExSet & toAdd, RequireExSet & toChange) +{ + enum class Decision + { + Undetermined, + Met, + Missing, + VersionNotSame, + LockedVersionNotSame + } decision = Decision::Undetermined; + + QString reqStr; + bool succeeded = true; + // list the composed requirements and say if they are met or unmet + for(auto & req: input) + { + do + { + if(req.equalsVersion.isEmpty()) + { + reqStr = QString("Req: %1").arg(req.uid); + if(index.contains(req.uid)) + { + decision = Decision::Met; + } + else + { + toAdd.insert(req); + decision = Decision::Missing; + } + break; + } + else + { + reqStr = QString("Req: %1 == %2").arg(req.uid, req.equalsVersion); + const auto & compIter = index.find(req.uid); + if(compIter == index.cend()) + { + toAdd.insert(req); + decision = Decision::Missing; + break; + } + auto & comp = (*compIter); + if(comp->getVersion() != req.equalsVersion) + { + if(comp->isCustom()) { + decision = Decision::LockedVersionNotSame; + } else { + if(comp->m_dependencyOnly) + { + decision = Decision::VersionNotSame; + } + else + { + decision = Decision::LockedVersionNotSame; + } + } + break; + } + decision = Decision::Met; + } + } while(false); + switch(decision) + { + case Decision::Undetermined: + qCritical() << "No decision for" << reqStr; + succeeded = false; + break; + case Decision::Met: + qDebug() << reqStr << "Is met."; + break; + case Decision::Missing: + qDebug() << reqStr << "Is missing and should be added at" << req.indexOfFirstDependee; + toAdd.insert(req); + break; + case Decision::VersionNotSame: + qDebug() << reqStr << "already has different version that can be changed."; + toChange.insert(req); + break; + case Decision::LockedVersionNotSame: + qDebug() << reqStr << "already has different version that cannot be changed."; + succeeded = false; + break; + } + } + return succeeded; +} + +// FIXME, TODO: decouple dependency resolution from loading +// FIXME: This works directly with the PackProfile internals. It shouldn't! It needs richer data types than PackProfile uses. +// FIXME: throw all this away and use a graph +void ComponentUpdateTask::resolveDependencies(bool checkOnly) +{ + qDebug() << "Resolving dependencies"; + /* + * this is a naive dependency resolving algorithm. all it does is check for following conditions and react in simple ways: + * 1. There are conflicting dependencies on the same uid with different exact version numbers + * -> hard error + * 2. A dependency has non-matching exact version number + * -> hard error + * 3. A dependency is entirely missing and needs to be injected before the dependee(s) + * -> requirements are injected + * + * NOTE: this is a placeholder and should eventually be replaced with something 'serious' + */ + auto & components = d->m_list->d->components; + auto & componentIndex = d->m_list->d->componentIndex; + + RequireExSet allRequires; + QStringList toRemove; + do + { + allRequires.clear(); + toRemove.clear(); + if(!gatherRequirementsFromComponents(components, allRequires)) + { + emitFailed(tr("Conflicting requirements detected during dependency checking!")); + return; + } + getTrivialRemovals(components, allRequires, toRemove); + if(!toRemove.isEmpty()) + { + qDebug() << "Removing obsolete components..."; + for(auto & remove : toRemove) + { + qDebug() << "Removing" << remove; + d->m_list->remove(remove); + } + } + } while (!toRemove.isEmpty()); + RequireExSet toAdd; + RequireExSet toChange; + bool succeeded = getTrivialComponentChanges(componentIndex, allRequires, toAdd, toChange); + if(!succeeded) + { + emitFailed(tr("Instance has conflicting dependencies.")); + return; + } + if(checkOnly) + { + if(toAdd.size() || toChange.size()) + { + emitFailed(tr("Instance has unresolved dependencies while loading/checking for launch.")); + } + else + { + emitSucceeded(); + } + return; + } + + bool recursionNeeded = false; + if(toAdd.size()) + { + // add stuff... + for(auto &add: toAdd) + { + ComponentPtr component = new Component(d->m_list, add.uid); + if(!add.equalsVersion.isEmpty()) + { + // exact version + qDebug() << "Adding" << add.uid << "version" << add.equalsVersion << "at position" << add.indexOfFirstDependee; + component->m_version = add.equalsVersion; + } + else + { + // version needs to be decided + qDebug() << "Adding" << add.uid << "at position" << add.indexOfFirstDependee; +// ############################################################################################################ +// HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. + if(!add.suggests.isEmpty()) + { + component->m_version = add.suggests; + } + else + { + if(add.uid == "org.lwjgl") + { + component->m_version = "2.9.1"; + } + else if (add.uid == "org.lwjgl3") + { + component->m_version = "3.1.2"; + } + else if (add.uid == "net.fabricmc.intermediary" || add.uid == "org.quiltmc.hashed") + { + auto minecraft = std::find_if(components.begin(), components.end(), [](ComponentPtr & cmp){ + return cmp->getID() == "net.minecraft"; + }); + if(minecraft != components.end()) { + component->m_version = (*minecraft)->getVersion(); + } + } + } +// HACK HACK HACK HACK FIXME: this is a placeholder for deciding what version to use. For now, it is hardcoded. +// ############################################################################################################ + } + component->m_dependencyOnly = true; + // FIXME: this should not work directly with the component list + d->m_list->insertComponent(add.indexOfFirstDependee, component); + componentIndex[add.uid] = component; + } + recursionNeeded = true; + } + if(toChange.size()) + { + // change a version of something that exists + for(auto &change: toChange) + { + // FIXME: this should not work directly with the component list + qDebug() << "Setting version of " << change.uid << "to" << change.equalsVersion; + auto component = componentIndex[change.uid]; + component->setVersion(change.equalsVersion); + } + recursionNeeded = true; + } + + if(recursionNeeded) + { + loadComponents(); + } + else + { + emitSucceeded(); + } +} + +void ComponentUpdateTask::remoteLoadSucceeded(size_t taskIndex) +{ + auto &taskSlot = d->remoteLoadStatusList[taskIndex]; + if(taskSlot.finished) + { + qWarning() << "Got multiple results from remote load task" << taskIndex; + return; + } + qDebug() << "Remote task" << taskIndex << "succeeded"; + taskSlot.succeeded = false; + taskSlot.finished = true; + d->remoteTasksInProgress --; + // update the cached data of the component from the downloaded version file. + if (taskSlot.type == RemoteLoadStatus::Type::Version) + { + auto component = d->m_list->getComponent(taskSlot.PackProfileIndex); + component->m_loaded = true; + component->updateCachedData(); + } + checkIfAllFinished(); +} + + +void ComponentUpdateTask::remoteLoadFailed(size_t taskIndex, const QString& msg) +{ + auto &taskSlot = d->remoteLoadStatusList[taskIndex]; + if(taskSlot.finished) + { + qWarning() << "Got multiple results from remote load task" << taskIndex; + return; + } + qDebug() << "Remote task" << taskIndex << "failed: " << msg; + d->remoteLoadSuccessful = false; + taskSlot.succeeded = false; + taskSlot.finished = true; + taskSlot.error = msg; + d->remoteTasksInProgress --; + checkIfAllFinished(); +} + +void ComponentUpdateTask::checkIfAllFinished() +{ + if(d->remoteTasksInProgress) + { + // not yet... + return; + } + if(d->remoteLoadSuccessful) + { + // nothing bad happened... clear the temp load status and proceed with looking at dependencies + d->remoteLoadStatusList.clear(); + resolveDependencies(d->mode == Mode::Launch); + } + else + { + // remote load failed... report error and bail + QStringList allErrorsList; + for(auto & item: d->remoteLoadStatusList) + { + if(!item.succeeded) + { + allErrorsList.append(item.error); + } + } + auto allErrors = allErrorsList.join("\n"); + emitFailed(tr("Component metadata update task failed while downloading from remote server:\n%1").arg(allErrors)); + d->remoteLoadStatusList.clear(); + } +} diff --git a/ultimmc/launcher/minecraft/ComponentUpdateTask.h b/ultimmc/launcher/minecraft/ComponentUpdateTask.h new file mode 100644 index 0000000..4274cab --- /dev/null +++ b/ultimmc/launcher/minecraft/ComponentUpdateTask.h @@ -0,0 +1,37 @@ +#pragma once + +#include "tasks/Task.h" +#include "net/Mode.h" + +#include +class PackProfile; +struct ComponentUpdateTaskData; + +class ComponentUpdateTask : public Task +{ + Q_OBJECT +public: + enum class Mode + { + Launch, + Resolution + }; + +public: + explicit ComponentUpdateTask(Mode mode, Net::Mode netmode, PackProfile * list, QObject *parent = 0); + virtual ~ComponentUpdateTask(); + +protected: + void executeTask(); + +private: + void loadComponents(); + void resolveDependencies(bool checkOnly); + + void remoteLoadSucceeded(size_t index); + void remoteLoadFailed(size_t index, const QString &msg); + void checkIfAllFinished(); + +private: + std::unique_ptr d; +}; diff --git a/ultimmc/launcher/minecraft/ComponentUpdateTask_p.h b/ultimmc/launcher/minecraft/ComponentUpdateTask_p.h new file mode 100644 index 0000000..5b02431 --- /dev/null +++ b/ultimmc/launcher/minecraft/ComponentUpdateTask_p.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include "net/Mode.h" + +class PackProfile; + +struct RemoteLoadStatus +{ + enum class Type + { + Index, + List, + Version + } type = Type::Version; + size_t PackProfileIndex = 0; + bool finished = false; + bool succeeded = false; + QString error; +}; + +struct ComponentUpdateTaskData +{ + PackProfile * m_list = nullptr; + QList remoteLoadStatusList; + bool remoteLoadSuccessful = true; + size_t remoteTasksInProgress = 0; + ComponentUpdateTask::Mode mode; + Net::Mode netmode; +}; diff --git a/ultimmc/launcher/minecraft/GradleSpecifier.h b/ultimmc/launcher/minecraft/GradleSpecifier.h new file mode 100644 index 0000000..d9bb020 --- /dev/null +++ b/ultimmc/launcher/minecraft/GradleSpecifier.h @@ -0,0 +1,151 @@ +#pragma once + +#include +#include +#include "DefaultVariable.h" + +struct GradleSpecifier +{ + GradleSpecifier() + { + m_valid = false; + } + GradleSpecifier(QString value) + { + operator=(value); + } + GradleSpecifier & operator =(const QString & value) + { + /* + org.gradle.test.classifiers : service : 1.0 : jdk15 @ jar + 0 "org.gradle.test.classifiers:service:1.0:jdk15@jar" + 1 "org.gradle.test.classifiers" + 2 "service" + 3 "1.0" + 4 "jdk15" + 5 "jar" + */ + QRegExp matcher("([^:@]+):([^:@]+):([^:@]+)" "(?::([^:@]+))?" "(?:@([^:@]+))?"); + m_valid = matcher.exactMatch(value); + if(!m_valid) { + m_invalidValue = value; + return *this; + } + auto elements = matcher.capturedTexts(); + m_groupId = elements[1]; + m_artifactId = elements[2]; + m_version = elements[3]; + m_classifier = elements[4]; + if(!elements[5].isEmpty()) + { + m_extension = elements[5]; + } + return *this; + } + QString serialize() const + { + if(!m_valid) { + return m_invalidValue; + } + QString retval = m_groupId + ":" + m_artifactId + ":" + m_version; + if(!m_classifier.isEmpty()) + { + retval += ":" + m_classifier; + } + if(m_extension.isExplicit()) + { + retval += "@" + m_extension; + } + return retval; + } + QString getFileName() const + { + if(!m_valid) { + return QString(); + } + QString filename = m_artifactId + '-' + m_version; + if(!m_classifier.isEmpty()) + { + filename += "-" + m_classifier; + } + filename += "." + m_extension; + return filename; + } + QString toPath(const QString & filenameOverride = QString()) const + { + if(!m_valid) { + return QString(); + } + QString filename; + if(filenameOverride.isEmpty()) + { + filename = getFileName(); + } + else + { + filename = filenameOverride; + } + QString path = m_groupId; + path.replace('.', '/'); + path += '/' + m_artifactId + '/' + m_version + '/' + filename; + return path; + } + inline bool valid() const + { + return m_valid; + } + inline QString version() const + { + return m_version; + } + inline QString groupId() const + { + return m_groupId; + } + inline QString artifactId() const + { + return m_artifactId; + } + inline void setClassifier(const QString & classifier) + { + m_classifier = classifier; + } + inline QString classifier() const + { + return m_classifier; + } + inline QString extension() const + { + return m_extension; + } + inline QString artifactPrefix() const + { + return m_groupId + ":" + m_artifactId; + } + bool matchName(const GradleSpecifier & other) const + { + return other.artifactId() == artifactId() && other.groupId() == groupId() && other.classifier() == classifier(); + } + bool operator==(const GradleSpecifier & other) const + { + if(m_groupId != other.m_groupId) + return false; + if(m_artifactId != other.m_artifactId) + return false; + if(m_version != other.m_version) + return false; + if(m_classifier != other.m_classifier) + return false; + if(m_extension != other.m_extension) + return false; + return true; + } +private: + QString m_invalidValue; + QString m_groupId; + QString m_artifactId; + QString m_version; + QString m_classifier; + DefaultVariable m_extension = DefaultVariable("jar"); + bool m_valid = false; +}; diff --git a/ultimmc/launcher/minecraft/GradleSpecifier_test.cpp b/ultimmc/launcher/minecraft/GradleSpecifier_test.cpp new file mode 100644 index 0000000..0900c9d --- /dev/null +++ b/ultimmc/launcher/minecraft/GradleSpecifier_test.cpp @@ -0,0 +1,78 @@ +#include +#include "TestUtil.h" + +#include "minecraft/GradleSpecifier.h" + +class GradleSpecifierTest : public QObject +{ + Q_OBJECT +private +slots: + void initTestCase() + { + + } + void cleanupTestCase() + { + + } + + void test_Positive_data() + { + QTest::addColumn("through"); + + QTest::newRow("3 parter") << "org.gradle.test.classifiers:service:1.0"; + QTest::newRow("classifier") << "org.gradle.test.classifiers:service:1.0:jdk15"; + QTest::newRow("jarextension") << "org.gradle.test.classifiers:service:1.0@jar"; + QTest::newRow("jarboth") << "org.gradle.test.classifiers:service:1.0:jdk15@jar"; + QTest::newRow("packxz") << "org.gradle.test.classifiers:service:1.0:jdk15@jar.pack.xz"; + } + void test_Positive() + { + QFETCH(QString, through); + + QString converted = GradleSpecifier(through).serialize(); + + QCOMPARE(converted, through); + } + + void test_Path_data() + { + QTest::addColumn("spec"); + QTest::addColumn("expected"); + + QTest::newRow("3 parter") << "group.id:artifact:1.0" << "group/id/artifact/1.0/artifact-1.0.jar"; + QTest::newRow("doom") << "id.software:doom:1.666:demons@wad" << "id/software/doom/1.666/doom-1.666-demons.wad"; + } + void test_Path() + { + QFETCH(QString, spec); + QFETCH(QString, expected); + + QString converted = GradleSpecifier(spec).toPath(); + + QCOMPARE(converted, expected); + } + void test_Negative_data() + { + QTest::addColumn("input"); + + QTest::newRow("too many :") << "org:gradle.test:class:::ifiers:service:1.0::"; + QTest::newRow("nonsense") << "I like turtles"; + QTest::newRow("empty string") << ""; + QTest::newRow("missing version") << "herp.derp:artifact"; + } + void test_Negative() + { + QFETCH(QString, input); + + GradleSpecifier spec(input); + QVERIFY(!spec.valid()); + QCOMPARE(spec.serialize(), input); + QCOMPARE(spec.toPath(), QString()); + } +}; + +QTEST_GUILESS_MAIN(GradleSpecifierTest) + +#include "GradleSpecifier_test.moc" diff --git a/ultimmc/launcher/minecraft/LaunchProfile.cpp b/ultimmc/launcher/minecraft/LaunchProfile.cpp new file mode 100644 index 0000000..4170518 --- /dev/null +++ b/ultimmc/launcher/minecraft/LaunchProfile.cpp @@ -0,0 +1,319 @@ +#include "LaunchProfile.h" +#include + +void LaunchProfile::clear() +{ + m_minecraftVersion.clear(); + m_minecraftVersionType.clear(); + m_minecraftAssets.reset(); + m_minecraftArguments.clear(); + m_tweakers.clear(); + m_mainClass.clear(); + m_appletClass.clear(); + m_libraries.clear(); + m_mavenFiles.clear(); + m_traits.clear(); + m_jarMods.clear(); + m_mainJar.reset(); + m_problemSeverity = ProblemSeverity::None; +} + +static void applyString(const QString & from, QString & to) +{ + if(from.isEmpty()) + return; + to = from; +} + +void LaunchProfile::applyMinecraftVersion(const QString& id) +{ + applyString(id, this->m_minecraftVersion); +} + +void LaunchProfile::applyAppletClass(const QString& appletClass) +{ + applyString(appletClass, this->m_appletClass); +} + +void LaunchProfile::applyMainClass(const QString& mainClass) +{ + applyString(mainClass, this->m_mainClass); +} + +void LaunchProfile::applyMinecraftArguments(const QString& minecraftArguments) +{ + applyString(minecraftArguments, this->m_minecraftArguments); +} + +void LaunchProfile::applyMinecraftVersionType(const QString& type) +{ + applyString(type, this->m_minecraftVersionType); +} + +void LaunchProfile::applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets) +{ + if(assets) + { + m_minecraftAssets = assets; + } +} + +void LaunchProfile::applyTraits(const QSet& traits) +{ + this->m_traits.unite(traits); +} + +void LaunchProfile::applyTweakers(const QStringList& tweakers) +{ + // if the applied tweakers override an existing one, skip it. this effectively moves it later in the sequence + QStringList newTweakers; + for(auto & tweaker: m_tweakers) + { + if (tweakers.contains(tweaker)) + { + continue; + } + newTweakers.append(tweaker); + } + // then just append the new tweakers (or moved original ones) + newTweakers += tweakers; + m_tweakers = newTweakers; +} + +void LaunchProfile::applyJarMods(const QList& jarMods) +{ + this->m_jarMods.append(jarMods); +} + +static int findLibraryByName(QList *haystack, const GradleSpecifier &needle) +{ + int retval = -1; + for (int i = 0; i < haystack->size(); ++i) + { + if (haystack->at(i)->rawName().matchName(needle)) + { + // only one is allowed. + if (retval != -1) + return -1; + retval = i; + } + } + return retval; +} + +void LaunchProfile::applyMods(const QList& mods) +{ + QList * list = &m_mods; + for(auto & mod: mods) + { + auto modCopy = Library::limitedCopy(mod); + + // find the mod by name. + const int index = findLibraryByName(list, mod->rawName()); + // mod not found? just add it. + if (index < 0) + { + list->append(modCopy); + return; + } + + auto existingLibrary = list->at(index); + // if we are higher it means we should update + if (Version(mod->version()) > Version(existingLibrary->version())) + { + list->replace(index, modCopy); + } + } +} + +void LaunchProfile::applyLibrary(LibraryPtr library) +{ + if(!library->isActive()) + { + return; + } + + QList * list = &m_libraries; + if(library->isNative()) + { + list = &m_nativeLibraries; + } + + auto libraryCopy = Library::limitedCopy(library); + + // find the library by name. + const int index = findLibraryByName(list, library->rawName()); + // library not found? just add it. + if (index < 0) + { + list->append(libraryCopy); + return; + } + + auto existingLibrary = list->at(index); + // if we are higher it means we should update + if (Version(library->version()) > Version(existingLibrary->version())) + { + list->replace(index, libraryCopy); + } +} + +void LaunchProfile::applyMavenFile(LibraryPtr mavenFile) +{ + if(!mavenFile->isActive()) + { + return; + } + + if(mavenFile->isNative()) + { + return; + } + + // unlike libraries, we do not keep only one version or try to dedupe them + m_mavenFiles.append(Library::limitedCopy(mavenFile)); +} + +const LibraryPtr LaunchProfile::getMainJar() const +{ + return m_mainJar; +} + +void LaunchProfile::applyMainJar(LibraryPtr jar) +{ + if(jar) + { + m_mainJar = jar; + } +} + +void LaunchProfile::applyProblemSeverity(ProblemSeverity severity) +{ + if (m_problemSeverity < severity) + { + m_problemSeverity = severity; + } +} + +const QList LaunchProfile::getProblems() const +{ + // FIXME: implement something that actually makes sense here + return {}; +} + +QString LaunchProfile::getMinecraftVersion() const +{ + return m_minecraftVersion; +} + +QString LaunchProfile::getAppletClass() const +{ + return m_appletClass; +} + +QString LaunchProfile::getMainClass() const +{ + return m_mainClass; +} + +const QSet &LaunchProfile::getTraits() const +{ + return m_traits; +} + +const QStringList & LaunchProfile::getTweakers() const +{ + return m_tweakers; +} + +bool LaunchProfile::hasTrait(const QString& trait) const +{ + return m_traits.contains(trait); +} + +ProblemSeverity LaunchProfile::getProblemSeverity() const +{ + return m_problemSeverity; +} + +QString LaunchProfile::getMinecraftVersionType() const +{ + return m_minecraftVersionType; +} + +std::shared_ptr LaunchProfile::getMinecraftAssets() const +{ + if(!m_minecraftAssets) + { + return std::make_shared("legacy"); + } + return m_minecraftAssets; +} + +QString LaunchProfile::getMinecraftArguments() const +{ + return m_minecraftArguments; +} + +const QList & LaunchProfile::getJarMods() const +{ + return m_jarMods; +} + +const QList & LaunchProfile::getLibraries() const +{ + return m_libraries; +} + +const QList & LaunchProfile::getNativeLibraries() const +{ + return m_nativeLibraries; +} + +const QList & LaunchProfile::getMavenFiles() const +{ + return m_mavenFiles; +} + +void LaunchProfile::getLibraryFiles( + const QString& architecture, + QStringList& jars, + QStringList& nativeJars, + const QString& overridePath, + const QString& tempPath +) const +{ + QStringList native32, native64; + jars.clear(); + nativeJars.clear(); + for (auto lib : getLibraries()) + { + lib->getApplicableFiles(currentSystem, jars, nativeJars, native32, native64, overridePath); + } + // NOTE: order is important here, add main jar last to the lists + if(m_mainJar) + { + // FIXME: HACK!! jar modding is weird and unsystematic! + if(m_jarMods.size()) + { + QDir tempDir(tempPath); + jars.append(tempDir.absoluteFilePath("minecraft.jar")); + } + else + { + m_mainJar->getApplicableFiles(currentSystem, jars, nativeJars, native32, native64, overridePath); + } + } + for (auto lib : getNativeLibraries()) + { + lib->getApplicableFiles(currentSystem, jars, nativeJars, native32, native64, overridePath); + } + if(architecture == "32") + { + nativeJars.append(native32); + } + else if(architecture == "64") + { + nativeJars.append(native64); + } +} diff --git a/ultimmc/launcher/minecraft/LaunchProfile.h b/ultimmc/launcher/minecraft/LaunchProfile.h new file mode 100644 index 0000000..c175253 --- /dev/null +++ b/ultimmc/launcher/minecraft/LaunchProfile.h @@ -0,0 +1,104 @@ +#pragma once +#include +#include "Library.h" +#include + +class LaunchProfile: public ProblemProvider +{ +public: + virtual ~LaunchProfile() {}; + +public: /* application of profile variables from patches */ + void applyMinecraftVersion(const QString& id); + void applyMainClass(const QString& mainClass); + void applyAppletClass(const QString& appletClass); + void applyMinecraftArguments(const QString& minecraftArguments); + void applyMinecraftVersionType(const QString& type); + void applyMinecraftAssets(MojangAssetIndexInfo::Ptr assets); + void applyTraits(const QSet &traits); + void applyTweakers(const QStringList &tweakers); + void applyJarMods(const QList &jarMods); + void applyMods(const QList &jarMods); + void applyLibrary(LibraryPtr library); + void applyMavenFile(LibraryPtr library); + void applyMainJar(LibraryPtr jar); + void applyProblemSeverity(ProblemSeverity severity); + /// clear the profile + void clear(); + +public: /* getters for profile variables */ + QString getMinecraftVersion() const; + QString getMainClass() const; + QString getAppletClass() const; + QString getMinecraftVersionType() const; + MojangAssetIndexInfo::Ptr getMinecraftAssets() const; + QString getMinecraftArguments() const; + const QSet & getTraits() const; + const QStringList & getTweakers() const; + const QList & getJarMods() const; + const QList & getLibraries() const; + const QList & getNativeLibraries() const; + const QList & getMavenFiles() const; + const LibraryPtr getMainJar() const; + void getLibraryFiles( + const QString & architecture, + QStringList & jars, + QStringList & nativeJars, + const QString & overridePath, + const QString & tempPath + ) const; + bool hasTrait(const QString & trait) const; + ProblemSeverity getProblemSeverity() const override; + const QList getProblems() const override; + +private: + /// the version of Minecraft - jar to use + QString m_minecraftVersion; + + /// Release type - "release" or "snapshot" + QString m_minecraftVersionType; + + /// Assets type - "legacy" or a version ID + MojangAssetIndexInfo::Ptr m_minecraftAssets; + + /** + * arguments that should be used for launching minecraft + * + * ex: "--username ${auth_player_name} --session ${auth_session} + * --version ${version_name} --gameDir ${game_directory} --assetsDir ${game_assets}" + */ + QString m_minecraftArguments; + + /// A list of all tweaker classes + QStringList m_tweakers; + + /// The main class to load first + QString m_mainClass; + + /// The applet class, for some very old minecraft releases + QString m_appletClass; + + /// the list of libraries + QList m_libraries; + + /// the list of maven files to be placed in the libraries folder, but not acted upon + QList m_mavenFiles; + + /// the main jar + LibraryPtr m_mainJar; + + /// the list of native libraries + QList m_nativeLibraries; + + /// traits, collected from all the version files (version files can only add) + QSet m_traits; + + /// A list of jar mods. version files can add those. + QList m_jarMods; + + /// the list of mods + QList m_mods; + + ProblemSeverity m_problemSeverity = ProblemSeverity::None; + +}; diff --git a/ultimmc/launcher/minecraft/Library.cpp b/ultimmc/launcher/minecraft/Library.cpp new file mode 100644 index 0000000..c798270 --- /dev/null +++ b/ultimmc/launcher/minecraft/Library.cpp @@ -0,0 +1,308 @@ +#include "Library.h" +#include "MinecraftInstance.h" + +#include +#include +#include +#include + + +void Library::getApplicableFiles(OpSys system, QStringList& jar, QStringList& native, QStringList& native32, + QStringList& native64, const QString &overridePath) const +{ + bool local = isLocal(); + auto actualPath = [&](QString relPath) + { + QFileInfo out(FS::PathCombine(storagePrefix(), relPath)); + if(local && !overridePath.isEmpty()) + { + QString fileName = out.fileName(); + return QFileInfo(FS::PathCombine(overridePath, fileName)).absoluteFilePath(); + } + return out.absoluteFilePath(); + }; + QString raw_storage = storageSuffix(system); + if(isNative()) + { + if (raw_storage.contains("${arch}")) + { + auto nat32Storage = raw_storage; + nat32Storage.replace("${arch}", "32"); + auto nat64Storage = raw_storage; + nat64Storage.replace("${arch}", "64"); + native32 += actualPath(nat32Storage); + native64 += actualPath(nat64Storage); + } + else + { + native += actualPath(raw_storage); + } + } + else + { + jar += actualPath(raw_storage); + } +} + +QList Library::getDownloads( + OpSys system, + class HttpMetaCache* cache, + QStringList& failedLocalFiles, + const QString & overridePath +) const +{ + QList out; + bool stale = isAlwaysStale(); + bool local = isLocal(); + + auto check_local_file = [&](QString storage) + { + QFileInfo fileinfo(storage); + QString fileName = fileinfo.fileName(); + auto fullPath = FS::PathCombine(overridePath, fileName); + QFileInfo localFileInfo(fullPath); + if(!localFileInfo.exists()) + { + failedLocalFiles.append(localFileInfo.filePath()); + return false; + } + return true; + }; + + auto add_download = [&](QString storage, QString url, QString sha1) + { + if(local) + { + return check_local_file(storage); + } + auto entry = cache->resolveEntry("libraries", storage); + if(stale) + { + entry->setStale(true); + } + if (!entry->isStale()) + return true; + Net::Download::Options options; + if(stale) + { + options |= Net::Download::Option::AcceptLocalFiles; + } + + if(sha1.size()) + { + auto rawSha1 = QByteArray::fromHex(sha1.toLatin1()); + auto dl = Net::Download::makeCached(url, entry, options); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + qDebug() << "Checksummed Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; + out.append(dl); + } + else + { + out.append(Net::Download::makeCached(url, entry, options)); + qDebug() << "Download for:" << rawName().serialize() << "storage:" << storage << "url:" << url; + } + return true; + }; + + QString raw_storage = storageSuffix(system); + if(m_mojangDownloads) + { + if(isNative()) + { + if(m_nativeClassifiers.contains(system)) + { + auto nativeClassifier = m_nativeClassifiers[system]; + if(nativeClassifier.contains("${arch}")) + { + auto nat32Classifier = nativeClassifier; + nat32Classifier.replace("${arch}", "32"); + auto nat64Classifier = nativeClassifier; + nat64Classifier.replace("${arch}", "64"); + auto nat32info = m_mojangDownloads->getDownloadInfo(nat32Classifier); + if(nat32info) + { + auto cooked_storage = raw_storage; + cooked_storage.replace("${arch}", "32"); + add_download(cooked_storage, nat32info->url, nat32info->sha1); + } + auto nat64info = m_mojangDownloads->getDownloadInfo(nat64Classifier); + if(nat64info) + { + auto cooked_storage = raw_storage; + cooked_storage.replace("${arch}", "64"); + add_download(cooked_storage, nat64info->url, nat64info->sha1); + } + } + else + { + auto info = m_mojangDownloads->getDownloadInfo(nativeClassifier); + if(info) + { + add_download(raw_storage, info->url, info->sha1); + } + } + } + else + { + qDebug() << "Ignoring native library" << m_name.serialize() << "because it has no classifier for current OS"; + } + } + else + { + if(m_mojangDownloads->artifact) + { + auto artifact = m_mojangDownloads->artifact; + add_download(raw_storage, artifact->url, artifact->sha1); + } + else + { + qDebug() << "Ignoring java library" << m_name.serialize() << "because it has no artifact"; + } + } + } + else + { + auto raw_dl = [&]() + { + if (!m_absoluteURL.isEmpty()) + { + return m_absoluteURL; + } + + if (m_repositoryURL.isEmpty()) + { + return BuildConfig.LIBRARY_BASE + raw_storage; + } + + if(m_repositoryURL.endsWith('/')) + { + return m_repositoryURL + raw_storage; + } + else + { + return m_repositoryURL + QChar('/') + raw_storage; + } + }(); + if (raw_storage.contains("${arch}")) + { + QString cooked_storage = raw_storage; + QString cooked_dl = raw_dl; + add_download(cooked_storage.replace("${arch}", "32"), cooked_dl.replace("${arch}", "32"), QString()); + cooked_storage = raw_storage; + cooked_dl = raw_dl; + add_download(cooked_storage.replace("${arch}", "64"), cooked_dl.replace("${arch}", "64"), QString()); + } + else + { + add_download(raw_storage, raw_dl, QString()); + } + } + return out; +} + +bool Library::isActive() const +{ + bool result = true; + if (m_rules.empty()) + { + result = true; + } + else + { + RuleAction ruleResult = Disallow; + for (auto rule : m_rules) + { + RuleAction temp = rule->apply(this); + if (temp != Defer) + ruleResult = temp; + } + result = result && (ruleResult == Allow); + } + if (isNative()) + { + result = result && m_nativeClassifiers.contains(currentSystem); + } + return result; +} + +bool Library::isLocal() const +{ + return m_hint == "local"; +} + +bool Library::isAlwaysStale() const +{ + return m_hint == "always-stale"; +} + +void Library::setStoragePrefix(QString prefix) +{ + m_storagePrefix = prefix; +} + +QString Library::defaultStoragePrefix() +{ + return "libraries/"; +} + +QString Library::storagePrefix() const +{ + if(m_storagePrefix.isEmpty()) + { + return defaultStoragePrefix(); + } + return m_storagePrefix; +} + +QString Library::filename(OpSys system) const +{ + if(!m_filename.isEmpty()) + { + return m_filename; + } + // non-native? use only the gradle specifier + if (!isNative()) + { + return m_name.getFileName(); + } + + // otherwise native, override classifiers. Mojang HACK! + GradleSpecifier nativeSpec = m_name; + if (m_nativeClassifiers.contains(system)) + { + nativeSpec.setClassifier(m_nativeClassifiers[system]); + } + else + { + nativeSpec.setClassifier("INVALID"); + } + return nativeSpec.getFileName(); +} + +QString Library::displayName(OpSys system) const +{ + if(!m_displayname.isEmpty()) + return m_displayname; + return filename(system); +} + +QString Library::storageSuffix(OpSys system) const +{ + // non-native? use only the gradle specifier + if (!isNative()) + { + return m_name.toPath(m_filename); + } + + // otherwise native, override classifiers. Mojang HACK! + GradleSpecifier nativeSpec = m_name; + if (m_nativeClassifiers.contains(system)) + { + nativeSpec.setClassifier(m_nativeClassifiers[system]); + } + else + { + nativeSpec.setClassifier("INVALID"); + } + return nativeSpec.toPath(m_filename); +} diff --git a/ultimmc/launcher/minecraft/Library.h b/ultimmc/launcher/minecraft/Library.h new file mode 100644 index 0000000..41d41a8 --- /dev/null +++ b/ultimmc/launcher/minecraft/Library.h @@ -0,0 +1,217 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Rule.h" +#include "minecraft/OpSys.h" +#include "GradleSpecifier.h" +#include "MojangDownloadInfo.h" + +class Library; +class MinecraftInstance; + +typedef std::shared_ptr LibraryPtr; + +class Library +{ + friend class OneSixVersionFormat; + friend class MojangVersionFormat; + friend class LibraryTest; +public: + Library() + { + } + Library(const QString &name) + { + m_name = name; + } + /// limited copy without some data. TODO: why? + static LibraryPtr limitedCopy(LibraryPtr base) + { + auto newlib = std::make_shared(); + newlib->m_name = base->m_name; + newlib->m_repositoryURL = base->m_repositoryURL; + newlib->m_hint = base->m_hint; + newlib->m_absoluteURL = base->m_absoluteURL; + newlib->m_extractExcludes = base->m_extractExcludes; + newlib->m_nativeClassifiers = base->m_nativeClassifiers; + newlib->m_rules = base->m_rules; + newlib->m_storagePrefix = base->m_storagePrefix; + newlib->m_mojangDownloads = base->m_mojangDownloads; + newlib->m_filename = base->m_filename; + return newlib; + } + +public: /* methods */ + /// Returns the raw name field + const GradleSpecifier & rawName() const + { + return m_name; + } + + void setRawName(const GradleSpecifier & spec) + { + m_name = spec; + } + + void setClassifier(const QString & spec) + { + m_name.setClassifier(spec); + } + + /// returns the full group and artifact prefix + QString artifactPrefix() const + { + return m_name.artifactPrefix(); + } + + /// get the artifact ID + QString artifactId() const + { + return m_name.artifactId(); + } + + /// get the artifact version + QString version() const + { + return m_name.version(); + } + + /// Returns true if the library is native + bool isNative() const + { + return m_nativeClassifiers.size() != 0; + } + + void setStoragePrefix(QString prefix = QString()); + + /// Set the url base for downloads + void setRepositoryURL(const QString &base_url) + { + m_repositoryURL = base_url; + } + + void getApplicableFiles(OpSys system, QStringList & jar, QStringList & native, + QStringList & native32, QStringList & native64, const QString & overridePath) const; + + void setAbsoluteUrl(const QString &absolute_url) + { + m_absoluteURL = absolute_url; + } + + void setFilename(const QString &filename) + { + m_filename = filename; + } + + /// Get the file name of the library + QString filename(OpSys system) const; + + // DEPRECATED: set a display name, used by jar mods only + void setDisplayName(const QString & displayName) + { + m_displayname = displayName; + } + + /// Get the file name of the library + QString displayName(OpSys system) const; + + void setMojangDownloadInfo(MojangLibraryDownloadInfo::Ptr info) + { + m_mojangDownloads = info; + } + + void setHint(const QString &hint) + { + m_hint = hint; + } + + /// Set the load rules + void setRules(QList> rules) + { + m_rules = rules; + } + + /// Returns true if the library should be loaded (or extracted, in case of natives) + bool isActive() const; + + /// Returns true if the library is contained in an instance and false if it is shared + bool isLocal() const; + + /// Returns true if the library is to always be checked for updates + bool isAlwaysStale() const; + + /// Return true if the library requires forge XZ hacks + bool isForge() const; + + // Get a list of downloads for this library + QList getDownloads(OpSys system, class HttpMetaCache * cache, + QStringList & failedLocalFiles, const QString & overridePath) const; + +private: /* methods */ + /// the default storage prefix used by MultiMC + static QString defaultStoragePrefix(); + + /// Get the prefix - root of the storage to be used + QString storagePrefix() const; + + /// Get the relative file path where the library should be saved + QString storageSuffix(OpSys system) const; + + QString hint() const + { + return m_hint; + } + +protected: /* data */ + /// the basic gradle dependency specifier. + GradleSpecifier m_name; + + /// DEPRECATED URL prefix of the maven repo where the file can be downloaded + QString m_repositoryURL; + + /// DEPRECATED: MultiMC-specific absolute URL. takes precedence over the implicit maven repo URL, if defined + QString m_absoluteURL; + + /// MultiMC extension - filename override + QString m_filename; + + /// DEPRECATED MultiMC extension - display name + QString m_displayname; + + /** + * MultiMC-specific type hint - modifies how the library is treated + */ + QString m_hint; + + /** + * storage - by default the local libraries folder in multimc, but could be elsewhere + * MultiMC specific, because of FTB. + */ + QString m_storagePrefix; + + /// true if the library had an extract/excludes section (even empty) + bool m_hasExcludes = false; + + /// a list of files that shouldn't be extracted from the library + QStringList m_extractExcludes; + + /// native suffixes per OS + QMap m_nativeClassifiers; + + /// true if the library had a rules section (even empty) + bool applyRules = false; + + /// rules associated with the library + QList> m_rules; + + /// MOJANG: container with Mojang style download info + MojangLibraryDownloadInfo::Ptr m_mojangDownloads; +}; diff --git a/ultimmc/launcher/minecraft/Library_test.cpp b/ultimmc/launcher/minecraft/Library_test.cpp new file mode 100644 index 0000000..47531ad --- /dev/null +++ b/ultimmc/launcher/minecraft/Library_test.cpp @@ -0,0 +1,272 @@ +#include +#include "TestUtil.h" + +#include "minecraft/MojangVersionFormat.h" +#include "minecraft/OneSixVersionFormat.h" +#include "minecraft/Library.h" +#include "net/HttpMetaCache.h" +#include "FileSystem.h" + +class LibraryTest : public QObject +{ + Q_OBJECT +private: + LibraryPtr readMojangJson(const char *file) + { + auto path = QFINDTESTDATA(file); + QFile jsonFile(path); + jsonFile.open(QIODevice::ReadOnly); + auto data = jsonFile.readAll(); + jsonFile.close(); + ProblemContainer problems; + return MojangVersionFormat::libraryFromJson(problems, QJsonDocument::fromJson(data).object(), file); + } + // get absolute path to expected storage, assuming default cache prefix + QStringList getStorage(QString relative) + { + return {FS::PathCombine(cache->getBasePath("libraries"), relative)}; + } +private +slots: + void initTestCase() + { + cache.reset(new HttpMetaCache()); + cache->addBase("libraries", QDir("libraries").absolutePath()); + dataDir = QDir("data").absolutePath(); + } + void test_legacy() + { + Library test("test.package:testname:testversion"); + QCOMPARE(test.artifactPrefix(), QString("test.package:testname")); + QCOMPARE(test.isNative(), false); + + QStringList jar, native, native32, native64; + test.getApplicableFiles(currentSystem, jar, native, native32, native64, QString()); + QCOMPARE(jar, getStorage("test/package/testname/testversion/testname-testversion.jar")); + QCOMPARE(native, {}); + QCOMPARE(native32, {}); + QCOMPARE(native64, {}); + } + void test_legacy_url() + { + QStringList failedFiles; + Library test("test.package:testname:testversion"); + test.setRepositoryURL("file://foo/bar"); + auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QString()); + QCOMPARE(downloads.size(), 1); + QCOMPARE(failedFiles, {}); + NetAction::Ptr dl = downloads[0]; + QCOMPARE(dl->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion.jar")); + } + void test_legacy_url_local_broken() + { + Library test("test.package:testname:testversion"); + QCOMPARE(test.isNative(), false); + QStringList failedFiles; + test.setHint("local"); + auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QString()); + QCOMPARE(downloads.size(), 0); + QCOMPARE(failedFiles, {"testname-testversion.jar"}); + } + void test_legacy_url_local_override() + { + Library test("com.paulscode:codecwav:20101023"); + QCOMPARE(test.isNative(), false); + QStringList failedFiles; + test.setHint("local"); + auto downloads = test.getDownloads(currentSystem, cache.get(), failedFiles, QString("data")); + QCOMPARE(downloads.size(), 0); + qDebug() << failedFiles; + QCOMPARE(failedFiles.size(), 0); + + QStringList jar, native, native32, native64; + test.getApplicableFiles(currentSystem, jar, native, native32, native64, QString("data")); + QCOMPARE(jar, {QFileInfo("data/codecwav-20101023.jar").absoluteFilePath()}); + QCOMPARE(native, {}); + QCOMPARE(native32, {}); + QCOMPARE(native64, {}); + } + void test_legacy_native() + { + Library test("test.package:testname:testversion"); + test.m_nativeClassifiers[OpSys::Os_Linux]="linux"; + QCOMPARE(test.isNative(), true); + test.setRepositoryURL("file://foo/bar"); + { + QStringList jar, native, native32, native64; + test.getApplicableFiles(Os_Linux, jar, native, native32, native64, QString()); + QCOMPARE(jar, {}); + QCOMPARE(native, getStorage("test/package/testname/testversion/testname-testversion-linux.jar")); + QCOMPARE(native32, {}); + QCOMPARE(native64, {}); + QStringList failedFiles; + auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QString()); + QCOMPARE(dls.size(), 1); + QCOMPARE(failedFiles, {}); + auto dl = dls[0]; + QCOMPARE(dl->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux.jar")); + } + } + void test_legacy_native_arch() + { + Library test("test.package:testname:testversion"); + test.m_nativeClassifiers[OpSys::Os_Linux]="linux-${arch}"; + test.m_nativeClassifiers[OpSys::Os_OSX]="osx-${arch}"; + test.m_nativeClassifiers[OpSys::Os_Windows]="windows-${arch}"; + QCOMPARE(test.isNative(), true); + test.setRepositoryURL("file://foo/bar"); + { + QStringList jar, native, native32, native64; + test.getApplicableFiles(Os_Linux, jar, native, native32, native64, QString()); + QCOMPARE(jar, {}); + QCOMPARE(native, {}); + QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-linux-32.jar")); + QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-linux-64.jar")); + QStringList failedFiles; + auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QString()); + QCOMPARE(dls.size(), 2); + QCOMPARE(failedFiles, {}); + QCOMPARE(dls[0]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux-32.jar")); + QCOMPARE(dls[1]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-linux-64.jar")); + } + { + QStringList jar, native, native32, native64; + test.getApplicableFiles(Os_Windows, jar, native, native32, native64, QString()); + QCOMPARE(jar, {}); + QCOMPARE(native, {}); + QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-windows-32.jar")); + QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-windows-64.jar")); + QStringList failedFiles; + auto dls = test.getDownloads(Os_Windows, cache.get(), failedFiles, QString()); + QCOMPARE(dls.size(), 2); + QCOMPARE(failedFiles, {}); + QCOMPARE(dls[0]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-windows-32.jar")); + QCOMPARE(dls[1]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-windows-64.jar")); + } + { + QStringList jar, native, native32, native64; + test.getApplicableFiles(Os_OSX, jar, native, native32, native64, QString()); + QCOMPARE(jar, {}); + QCOMPARE(native, {}); + QCOMPARE(native32, getStorage("test/package/testname/testversion/testname-testversion-osx-32.jar")); + QCOMPARE(native64, getStorage("test/package/testname/testversion/testname-testversion-osx-64.jar")); + QStringList failedFiles; + auto dls = test.getDownloads(Os_OSX, cache.get(), failedFiles, QString()); + QCOMPARE(dls.size(), 2); + QCOMPARE(failedFiles, {}); + QCOMPARE(dls[0]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-osx-32.jar")); + QCOMPARE(dls[1]->m_url, QUrl("file://foo/bar/test/package/testname/testversion/testname-testversion-osx-64.jar")); + } + } + void test_legacy_native_arch_local_override() + { + Library test("test.package:testname:testversion"); + test.m_nativeClassifiers[OpSys::Os_Linux]="linux-${arch}"; + test.setHint("local"); + QCOMPARE(test.isNative(), true); + test.setRepositoryURL("file://foo/bar"); + { + QStringList jar, native, native32, native64; + test.getApplicableFiles(Os_Linux, jar, native, native32, native64, QString("data")); + QCOMPARE(jar, {}); + QCOMPARE(native, {}); + QCOMPARE(native32, {QFileInfo("data/testname-testversion-linux-32.jar").absoluteFilePath()}); + QCOMPARE(native64, {QFileInfo("data/testname-testversion-linux-64.jar").absoluteFilePath()}); + QStringList failedFiles; + auto dls = test.getDownloads(Os_Linux, cache.get(), failedFiles, QString("data")); + QCOMPARE(dls.size(), 0); + QCOMPARE(failedFiles, {"data/testname-testversion-linux-64.jar"}); + } + } + void test_onenine() + { + auto test = readMojangJson("data/lib-simple.json"); + { + QStringList jar, native, native32, native64; + test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString()); + QCOMPARE(jar, getStorage("com/paulscode/codecwav/20101023/codecwav-20101023.jar")); + QCOMPARE(native, {}); + QCOMPARE(native32, {}); + QCOMPARE(native64, {}); + } + { + QStringList failedFiles; + auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QString()); + QCOMPARE(dls.size(), 1); + QCOMPARE(failedFiles, {}); + QCOMPARE(dls[0]->m_url, QUrl("https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar")); + } + test->setHint("local"); + { + QStringList jar, native, native32, native64; + test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString("data")); + QCOMPARE(jar, {QFileInfo("data/codecwav-20101023.jar").absoluteFilePath()}); + QCOMPARE(native, {}); + QCOMPARE(native32, {}); + QCOMPARE(native64, {}); + } + { + QStringList failedFiles; + auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QString("data")); + QCOMPARE(dls.size(), 0); + QCOMPARE(failedFiles, {}); + } + } + void test_onenine_local_override() + { + auto test = readMojangJson("data/lib-simple.json"); + test->setHint("local"); + { + QStringList jar, native, native32, native64; + test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString("data")); + QCOMPARE(jar, {QFileInfo("data/codecwav-20101023.jar").absoluteFilePath()}); + QCOMPARE(native, {}); + QCOMPARE(native32, {}); + QCOMPARE(native64, {}); + } + { + QStringList failedFiles; + auto dls = test->getDownloads(Os_Linux, cache.get(), failedFiles, QString("data")); + QCOMPARE(dls.size(), 0); + QCOMPARE(failedFiles, {}); + } + } + void test_onenine_native() + { + auto test = readMojangJson("data/lib-native.json"); + QStringList jar, native, native32, native64; + test->getApplicableFiles(Os_OSX, jar, native, native32, native64, QString()); + QCOMPARE(jar, QStringList()); + QCOMPARE(native, getStorage("org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar")); + QCOMPARE(native32, {}); + QCOMPARE(native64, {}); + QStringList failedFiles; + auto dls = test->getDownloads(Os_OSX, cache.get(), failedFiles, QString()); + QCOMPARE(dls.size(), 1); + QCOMPARE(failedFiles, {}); + QCOMPARE(dls[0]->m_url, QUrl("https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar")); + } + void test_onenine_native_arch() + { + auto test = readMojangJson("data/lib-native-arch.json"); + QStringList jar, native, native32, native64; + test->getApplicableFiles(Os_Windows, jar, native, native32, native64, QString()); + QCOMPARE(jar, {}); + QCOMPARE(native, {}); + QCOMPARE(native32, getStorage("tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar")); + QCOMPARE(native64, getStorage("tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar")); + QStringList failedFiles; + auto dls = test->getDownloads(Os_Windows, cache.get(), failedFiles, QString()); + QCOMPARE(dls.size(), 2); + QCOMPARE(failedFiles, {}); + QCOMPARE(dls[0]->m_url, QUrl("https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar")); + QCOMPARE(dls[1]->m_url, QUrl("https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar")); + } +private: + std::unique_ptr cache; + QString dataDir; +}; + +QTEST_GUILESS_MAIN(LibraryTest) + +#include "Library_test.moc" diff --git a/ultimmc/launcher/minecraft/MinecraftInstance.cpp b/ultimmc/launcher/minecraft/MinecraftInstance.cpp new file mode 100644 index 0000000..06c2f0a --- /dev/null +++ b/ultimmc/launcher/minecraft/MinecraftInstance.cpp @@ -0,0 +1,1109 @@ +#include "MinecraftInstance.h" +#include "minecraft/launch/CreateGameFolders.h" +#include "minecraft/launch/ExtractNatives.h" +#include "minecraft/launch/PrintInstanceInfo.h" +#include "settings/Setting.h" +#include "settings/SettingsObject.h" +#include "Application.h" + +#include "MMCStrings.h" +#include "pathmatcher/RegexpMatcher.h" +#include "pathmatcher/MultiMatcher.h" +#include "FileSystem.h" +#include "java/JavaVersion.h" +#include "MMCTime.h" + +#include "launch/LaunchTask.h" +#include "launch/steps/LookupServerAddress.h" +#include "launch/steps/PostLaunchCommand.h" +#include "launch/steps/Update.h" +#include "launch/steps/PreLaunchCommand.h" +#include "launch/steps/TextPrint.h" +#include "launch/steps/CheckJava.h" + +#include "minecraft/launch/LauncherPartLaunch.h" +#include "minecraft/launch/DirectJavaLaunch.h" +#include "minecraft/launch/ModMinecraftJar.h" +#include "minecraft/launch/ClaimAccount.h" +#include "minecraft/launch/ReconstructAssets.h" +#include "minecraft/launch/ScanModFolders.h" +#include "minecraft/launch/InjectAuthlib.h" +#include "minecraft/launch/VerifyJavaInstall.h" +#include "minecraft/auth/AccountList.h" + +#include "java/JavaUtils.h" + +#include "meta/Index.h" +#include "meta/VersionList.h" + +#include "icons/IconList.h" + +#include "mod/ModFolderModel.h" +#include "mod/ResourcePackFolderModel.h" +#include "mod/TexturePackFolderModel.h" + +#include "WorldList.h" + +#include "PackProfile.h" +#include "AssetsUtils.h" +#include "MinecraftUpdate.h" +#include "MinecraftLoadAndCheck.h" +#include "minecraft/gameoptions/GameOptions.h" +#include "minecraft/update/FoldersTask.h" +#include "minecraft/VersionFilterData.h" + +#define IBUS "@im=ibus" + +// all of this because keeping things compatible with deprecated old settings +// if either of the settings {a, b} is true, this also resolves to true +class OrSetting : public Setting +{ + Q_OBJECT +public: + OrSetting(QString id, std::shared_ptr a, std::shared_ptr b) + :Setting({id}, false), m_a(a), m_b(b) + { + } + virtual QVariant get() const + { + bool a = m_a->get().toBool(); + bool b = m_b->get().toBool(); + return a || b; + } + virtual void reset() {} + virtual void set(QVariant value) {} +private: + std::shared_ptr m_a; + std::shared_ptr m_b; +}; + +MinecraftInstance::MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) + : BaseInstance(globalSettings, settings, rootDir) +{ + // Java Settings + auto javaOverride = m_settings->registerSetting("OverrideJava", false); + auto locationOverride = m_settings->registerSetting("OverrideJavaLocation", false); + auto argsOverride = m_settings->registerSetting("OverrideJavaArgs", false); + + // combinations + auto javaOrLocation = std::make_shared("JavaOrLocationOverride", javaOverride, locationOverride); + auto javaOrArgs = std::make_shared("JavaOrArgsOverride", javaOverride, argsOverride); + + m_settings->registerOverride(globalSettings->getSetting("JavaPath"), javaOrLocation); + m_settings->registerOverride(globalSettings->getSetting("JvmArgs"), javaOrArgs); + + // special! + m_settings->registerPassthrough(globalSettings->getSetting("JavaTimestamp"), javaOrLocation); + m_settings->registerPassthrough(globalSettings->getSetting("JavaVersion"), javaOrLocation); + m_settings->registerPassthrough(globalSettings->getSetting("JavaArchitecture"), javaOrLocation); + + // Window Size + auto windowSetting = m_settings->registerSetting("OverrideWindow", false); + m_settings->registerOverride(globalSettings->getSetting("LaunchMaximized"), windowSetting); + m_settings->registerOverride(globalSettings->getSetting("MinecraftWinWidth"), windowSetting); + m_settings->registerOverride(globalSettings->getSetting("MinecraftWinHeight"), windowSetting); + + // Memory + auto memorySetting = m_settings->registerSetting("OverrideMemory", false); + m_settings->registerOverride(globalSettings->getSetting("MinMemAlloc"), memorySetting); + m_settings->registerOverride(globalSettings->getSetting("MaxMemAlloc"), memorySetting); + m_settings->registerOverride(globalSettings->getSetting("PermGen"), memorySetting); + + // Minecraft launch method + auto launchMethodOverride = m_settings->registerSetting("OverrideMCLaunchMethod", false); + m_settings->registerOverride(globalSettings->getSetting("MCLaunchMethod"), launchMethodOverride); + + // Native library workarounds + auto nativeLibraryWorkaroundsOverride = m_settings->registerSetting("OverrideNativeWorkarounds", false); + m_settings->registerOverride(globalSettings->getSetting("UseNativeOpenAL"), nativeLibraryWorkaroundsOverride); + m_settings->registerOverride(globalSettings->getSetting("UseNativeGLFW"), nativeLibraryWorkaroundsOverride); + + // Game time + auto gameTimeOverride = m_settings->registerSetting("OverrideGameTime", false); + m_settings->registerOverride(globalSettings->getSetting("ShowGameTime"), gameTimeOverride); + m_settings->registerOverride(globalSettings->getSetting("RecordGameTime"), gameTimeOverride); + + // Join server on launch, this does not have a global override + m_settings->registerSetting("JoinWorldOnLaunch", false); + m_settings->registerSetting("JoinServerOnLaunch", false); + m_settings->registerSetting("JoinServerOnLaunchAddress", ""); + m_settings->registerSetting("JoinSingleplayerWorldOnLaunch", false); + m_settings->registerSetting("JoinSingleplayerWorldOnLaunchName", ""); + + // DEPRECATED: Read what versions the user configuration thinks should be used + m_settings->registerSetting({"IntendedVersion", "MinecraftVersion"}, ""); + m_settings->registerSetting("LWJGLVersion", ""); + m_settings->registerSetting("ForgeVersion", ""); + m_settings->registerSetting("LiteloaderVersion", ""); + + m_components.reset(new PackProfile(this)); + m_components->setOldConfigVersion("net.minecraft", m_settings->get("IntendedVersion").toString()); + auto setting = m_settings->getSetting("LWJGLVersion"); + m_components->setOldConfigVersion("org.lwjgl", m_settings->get("LWJGLVersion").toString()); + m_components->setOldConfigVersion("net.minecraftforge", m_settings->get("ForgeVersion").toString()); + m_components->setOldConfigVersion("com.mumfrey.liteloader", m_settings->get("LiteloaderVersion").toString()); +} + +void MinecraftInstance::saveNow() +{ + m_components->saveNow(); +} + +QString MinecraftInstance::typeName() const +{ + return "Minecraft"; +} + +std::shared_ptr MinecraftInstance::getPackProfile() const +{ + return m_components; +} + +QSet MinecraftInstance::traits() const +{ + auto components = getPackProfile(); + if (!components) + { + return {"version-incomplete"}; + } + auto profile = components->getProfile(); + if (!profile) + { + return {"version-incomplete"}; + } + return profile->getTraits(); +} + +QString MinecraftInstance::gameRoot() const +{ + QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); + QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft")); + + if (mcDir.exists() && !dotMCDir.exists()) + return mcDir.filePath(); + else + return dotMCDir.filePath(); +} + +QString MinecraftInstance::binRoot() const +{ + return FS::PathCombine(gameRoot(), "bin"); +} + +QString MinecraftInstance::getNativePath() const +{ + QDir natives_dir(FS::PathCombine(instanceRoot(), "natives/")); + return natives_dir.absolutePath(); +} + +QString MinecraftInstance::getLocalLibraryPath() const +{ + QDir libraries_dir(FS::PathCombine(instanceRoot(), "libraries/")); + return libraries_dir.absolutePath(); +} + +QString MinecraftInstance::jarModsDir() const +{ + QDir jarmods_dir(FS::PathCombine(instanceRoot(), "jarmods/")); + return jarmods_dir.absolutePath(); +} + +QString MinecraftInstance::modsRoot() const +{ + return FS::PathCombine(gameRoot(), "mods"); +} + +QString MinecraftInstance::modsCacheLocation() const +{ + return FS::PathCombine(instanceRoot(), "mods.cache"); +} + +QString MinecraftInstance::coreModsDir() const +{ + return FS::PathCombine(gameRoot(), "coremods"); +} + +QString MinecraftInstance::resourcePacksDir() const +{ + return FS::PathCombine(gameRoot(), "resourcepacks"); +} + +QString MinecraftInstance::texturePacksDir() const +{ + return FS::PathCombine(gameRoot(), "texturepacks"); +} + +QString MinecraftInstance::shaderPacksDir() const +{ + return FS::PathCombine(gameRoot(), "shaderpacks"); +} + +QString MinecraftInstance::instanceConfigFolder() const +{ + return FS::PathCombine(gameRoot(), "config"); +} + +QString MinecraftInstance::libDir() const +{ + return FS::PathCombine(gameRoot(), "lib"); +} + +QString MinecraftInstance::worldDir() const +{ + return FS::PathCombine(gameRoot(), "saves"); +} + +QString MinecraftInstance::resourcesDir() const +{ + return FS::PathCombine(gameRoot(), "resources"); +} + +QDir MinecraftInstance::librariesPath() const +{ + return QDir::current().absoluteFilePath("libraries"); +} + +QDir MinecraftInstance::jarmodsPath() const +{ + return QDir(jarModsDir()); +} + +QDir MinecraftInstance::versionsPath() const +{ + return QDir::current().absoluteFilePath("versions"); +} + +QStringList MinecraftInstance::getClassPath() const +{ + QStringList jars, nativeJars; + auto javaArchitecture = settings()->get("JavaArchitecture").toString(); + auto profile = m_components->getProfile(); + profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot()); + return jars; +} + +QString MinecraftInstance::getMainClass() const +{ + auto profile = m_components->getProfile(); + return profile->getMainClass(); +} + +QStringList MinecraftInstance::getNativeJars() const +{ + QStringList jars, nativeJars; + auto javaArchitecture = settings()->get("JavaArchitecture").toString(); + auto profile = m_components->getProfile(); + profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot()); + return nativeJars; +} + +QStringList MinecraftInstance::extraArguments() const +{ + auto list = BaseInstance::extraArguments(); + auto version = getPackProfile(); + if (!version) + return list; + auto jarMods = getJarMods(); + if (!jarMods.isEmpty()) + { + list.append({"-Dfml.ignoreInvalidMinecraftCertificates=true", + "-Dfml.ignorePatchDiscrepancies=true"}); + } + return list; +} + +QStringList MinecraftInstance::javaArguments() const +{ + QStringList args; + + // custom args go first. we want to override them if we have our own here. + args.append(extraArguments()); + + // OSX dock icon and name +#ifdef Q_OS_MAC + args << "-Xdock:icon=icon.png"; + args << QString("-Xdock:name=\"%1\"").arg(windowTitle()); +#endif + auto traits_ = traits(); + // HACK: fix issues on macOS with 1.13 snapshots + // NOTE: Oracle Java option. if there are alternate jvm implementations, this would be the place to customize this for them +#ifdef Q_OS_MAC + if(traits_.contains("FirstThreadOnMacOS")) + { + args << QString("-XstartOnFirstThread"); + } +#endif + + // HACK: Stupid hack for Intel drivers. See: https://mojang.atlassian.net/browse/MCL-767 +#ifdef Q_OS_WIN32 + args << QString("-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_" + "minecraft.exe.heapdump"); +#endif + + int min = settings()->get("MinMemAlloc").toInt(); + int max = settings()->get("MaxMemAlloc").toInt(); + if(min < max) + { + args << QString("-Xms%1m").arg(min); + args << QString("-Xmx%1m").arg(max); + } + else + { + args << QString("-Xms%1m").arg(max); + args << QString("-Xmx%1m").arg(min); + } + + // No PermGen in newer java. + JavaVersion javaVersion = getJavaVersion(); + if(javaVersion.requiresPermGen()) + { + auto permgen = settings()->get("PermGen").toInt(); + if (permgen != 64) + { + args << QString("-XX:PermSize=%1m").arg(permgen); + } + } + + args << "-Duser.language=en"; + + if (m_injector) { + args << m_injector->javaArg; + args << "-Dauthlibinjector.noShowServerName"; + } + + return args; +} + +QMap MinecraftInstance::getVariables() const +{ + QMap out; + out.insert("INST_NAME", name()); + out.insert("INST_ID", id()); + out.insert("INST_DIR", QDir(instanceRoot()).absolutePath()); + out.insert("INST_MC_DIR", QDir(gameRoot()).absolutePath()); + out.insert("INST_JAVA", settings()->get("JavaPath").toString()); + out.insert("INST_JAVA_ARGS", javaArguments().join(' ')); + return out; +} + +QProcessEnvironment MinecraftInstance::createEnvironment() +{ + // prepare the process environment + QProcessEnvironment env = CleanEnviroment(); + + // export some infos + auto variables = getVariables(); + for (auto it = variables.begin(); it != variables.end(); ++it) + { + env.insert(it.key(), it.value()); + } + return env; +} + +static QString replaceTokensIn(QString text, QMap with) +{ + QString result; + QRegExp token_regexp("\\$\\{(.+)\\}"); + token_regexp.setMinimal(true); + QStringList list; + int tail = 0; + int head = 0; + while ((head = token_regexp.indexIn(text, head)) != -1) + { + result.append(text.mid(tail, head - tail)); + QString key = token_regexp.cap(1); + auto iter = with.find(key); + if (iter != with.end()) + { + result.append(*iter); + } + head += token_regexp.matchedLength(); + tail = head; + } + result.append(text.mid(tail)); + return result; +} + +QStringList MinecraftInstance::processMinecraftArgs( + AuthSessionPtr session, QuickPlayTargetPtr quickPlayTarget) const +{ + auto profile = m_components->getProfile(); + QString args_pattern = profile->getMinecraftArguments(); + for (auto tweaker : profile->getTweakers()) + { + args_pattern += " --tweakClass " + tweaker; + } + + if (quickPlayTarget && !quickPlayTarget->address.isEmpty()) + { + if (m_components->getComponent("net.minecraft")->getReleaseDateTime() >= g_VersionFilterData.quickPlayBeginsDate) + { + args_pattern += " --quickPlayMultiplayer " + quickPlayTarget->address + ":" + QString::number(quickPlayTarget->port); + } + else + { + args_pattern += " --server " + quickPlayTarget->address; + args_pattern += " --port " + QString::number(quickPlayTarget->port); + } + } + + if (quickPlayTarget && !quickPlayTarget->world.isEmpty()) + { + args_pattern += " --quickPlaySingleplayer \"" + quickPlayTarget->world + "\""; + } + + QMap token_mapping; + // yggdrasil! + if(session) { + // token_mapping["auth_username"] = session->username; + token_mapping["auth_session"] = session->session; + token_mapping["auth_access_token"] = session->access_token; + token_mapping["auth_player_name"] = session->player_name; + token_mapping["auth_uuid"] = session->uuid; + token_mapping["user_properties"] = session->serializeUserProperties(); + token_mapping["user_type"] = session->user_type; + if(session->demo) { + args_pattern += " --demo"; + } + } + + token_mapping["profile_name"] = name(); + token_mapping["version_name"] = profile->getMinecraftVersion(); + token_mapping["version_type"] = profile->getMinecraftVersionType(); + + QString absRootDir = QDir(gameRoot()).absolutePath(); + token_mapping["game_directory"] = absRootDir; + QString absAssetsDir = QDir("assets/").absolutePath(); + auto assets = profile->getMinecraftAssets(); + token_mapping["game_assets"] = AssetsUtils::getAssetsDir(assets->id, resourcesDir()).absolutePath(); + + // 1.7.3+ assets tokens + token_mapping["assets_root"] = absAssetsDir; + token_mapping["assets_index_name"] = assets->id; + + QStringList parts = args_pattern.split(' ', QString::SkipEmptyParts); + for (int i = 0; i < parts.length(); i++) + { + parts[i] = replaceTokensIn(parts[i], token_mapping); + } + return parts; +} + +QString MinecraftInstance::createLaunchScript(AuthSessionPtr session, QuickPlayTargetPtr quickPlayTarget) +{ + QString launchScript; + + if (!m_components) + return QString(); + auto profile = m_components->getProfile(); + if(!profile) + return QString(); + + auto mainClass = getMainClass(); + if (!mainClass.isEmpty()) + { + launchScript += "mainClass " + mainClass + "\n"; + } + auto appletClass = profile->getAppletClass(); + if (!appletClass.isEmpty()) + { + launchScript += "appletClass " + appletClass + "\n"; + } + + if (quickPlayTarget && !quickPlayTarget->address.isEmpty()) + { + launchScript += "useQuickPlay " + QString::number(m_components->getComponent("net.minecraft")->getReleaseDateTime() >= g_VersionFilterData.quickPlayBeginsDate) + "\n"; + launchScript += "serverAddress " + quickPlayTarget->address + "\n"; + launchScript += "serverPort " + QString::number(quickPlayTarget->port) + "\n"; + } + + if (quickPlayTarget && !quickPlayTarget->world.isEmpty()) + { + launchScript += "joinWorld " + quickPlayTarget->world + "\n"; + } + + // generic minecraft params + for (auto param : processMinecraftArgs( + session, + nullptr /* When using a launch script, the server and world parameters are handled by it*/ + )) + { + launchScript += "param " + param + "\n"; + } + + // window size, title and state, legacy + { + QString windowParams; + if (settings()->get("LaunchMaximized").toBool()) + windowParams = "max"; + else + windowParams = QString("%1x%2") + .arg(settings()->get("MinecraftWinWidth").toInt()) + .arg(settings()->get("MinecraftWinHeight").toInt()); + launchScript += "windowTitle " + windowTitle() + "\n"; + launchScript += "windowParams " + windowParams + "\n"; + launchScript += "instanceTitle " + instanceTitle() + "\n"; + launchScript += "instanceIconId " + iconKey() + "\n"; + } + + // legacy auth + if(session) + { + launchScript += "userName " + session->player_name + "\n"; + launchScript += "sessionId " + session->session + "\n"; + } + + // libraries and class path. + { + QStringList jars, nativeJars; + auto javaArchitecture = settings()->get("JavaArchitecture").toString(); + profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot()); + for(auto file: jars) + { + launchScript += "cp " + file + "\n"; + } + for(auto file: nativeJars) + { + launchScript += "ext " + file + "\n"; + } + launchScript += "natives " + getNativePath() + "\n"; + } + + for (auto trait : profile->getTraits()) + { + launchScript += "traits " + trait + "\n"; + } + launchScript += "launcher onesix\n"; + // qDebug() << "Generated launch script:" << launchScript; + return launchScript; +} + +QStringList MinecraftInstance::verboseDescription(AuthSessionPtr session, QuickPlayTargetPtr quickPlayTarget) +{ + QStringList out; + out << "Main Class:" << " " + getMainClass() << ""; + out << "Native path:" << " " + getNativePath() << ""; + + auto profile = m_components->getProfile(); + + auto alltraits = traits(); + if(alltraits.size()) + { + out << "Traits:"; + for (auto trait : alltraits) + { + out << "traits " + trait; + } + out << ""; + } + + auto settings = this->settings(); + bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); + bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); + if (nativeOpenAL || nativeGLFW) + { + if (nativeOpenAL) + out << "Using system OpenAL."; + if (nativeGLFW) + out << "Using system GLFW."; + out << ""; + } + + // libraries and class path. + { + out << "Libraries:"; + QStringList jars, nativeJars; + auto javaArchitecture = settings->get("JavaArchitecture").toString(); + profile->getLibraryFiles(javaArchitecture, jars, nativeJars, getLocalLibraryPath(), binRoot()); + auto printLibFile = [&](const QString & path) + { + QFileInfo info(path); + if(info.exists()) + { + out << " " + path; + } + else + { + out << " " + path + " (missing)"; + } + }; + for(auto file: jars) + { + printLibFile(file); + } + out << ""; + out << "Native libraries:"; + for(auto file: nativeJars) + { + printLibFile(file); + } + out << ""; + } + + auto printModList = [&](const QString & label, ModFolderModel & model) { + if(model.size()) + { + out << QString("%1:").arg(label); + auto modList = model.allMods(); + std::sort(modList.begin(), modList.end(), [](Mod &a, Mod &b) { + auto aName = a.filename().completeBaseName(); + auto bName = b.filename().completeBaseName(); + return aName.localeAwareCompare(bName) < 0; + }); + for(auto & mod: modList) + { + if(mod.type() == Mod::MOD_FOLDER) + { + out << u8" [📁] " + mod.filename().fileName() + " (folder)"; + continue; + } + + if(mod.enabled()) { + out << u8" [✔️] " + mod.filename().fileName(); + } + else { + out << u8" [❌] " + mod.filename().fileName() + " (disabled)"; + } + + } + out << ""; + } + }; + + printModList("Mods", *(loaderModList().get())); + printModList("Core Mods", *(coreModList().get())); + + auto & jarMods = profile->getJarMods(); + if(jarMods.size()) + { + out << "Jar Mods:"; + for(auto & jarmod: jarMods) + { + auto displayname = jarmod->displayName(currentSystem); + auto realname = jarmod->filename(currentSystem); + if(displayname != realname) + { + out << " " + displayname + " (" + realname + ")"; + } + else + { + out << " " + realname; + } + } + out << ""; + } + + auto params = processMinecraftArgs(session, quickPlayTarget); + out << "Params:"; + out << " " + params.join(' '); + out << ""; + + QString windowParams; + if (settings->get("LaunchMaximized").toBool()) + { + out << "Window size: max (if available)"; + } + else + { + auto width = settings->get("MinecraftWinWidth").toInt(); + auto height = settings->get("MinecraftWinHeight").toInt(); + out << "Window size: " + QString::number(width) + " x " + QString::number(height); + } + out << ""; + return out; +} + +QMap MinecraftInstance::createCensorFilterFromSession(AuthSessionPtr session) +{ + if(!session) + { + return QMap(); + } + auto & sessionRef = *session.get(); + QMap filter; + auto addToFilter = [&filter](QString key, QString value) + { + if(key.trimmed().size()) + { + filter[key] = value; + } + }; + if (sessionRef.session != "-") + { + addToFilter(sessionRef.session, tr("")); + } + addToFilter(sessionRef.access_token, tr("")); + if(sessionRef.client_token.size()) { + addToFilter(sessionRef.client_token, tr("")); + } + addToFilter(sessionRef.uuid, tr("")); + + return filter; +} + +MessageLevel::Enum MinecraftInstance::guessLevel(const QString &line, MessageLevel::Enum level) +{ + QRegularExpression re("\\[(?[0-9:]+)\\] \\[[^/]+/(?[^\\]]+)\\]"); + auto match = re.match(line); + if(match.hasMatch()) + { + // New style logs from log4j + QString timestamp = match.captured("timestamp"); + QString levelStr = match.captured("level"); + if(levelStr == "INFO") + level = MessageLevel::Message; + if(levelStr == "WARN") + level = MessageLevel::Warning; + if(levelStr == "ERROR") + level = MessageLevel::Error; + if(levelStr == "FATAL") + level = MessageLevel::Fatal; + if(levelStr == "TRACE" || levelStr == "DEBUG") + level = MessageLevel::Debug; + } + else + { + // Old style forge logs + if (line.contains("[INFO]") || line.contains("[CONFIG]") || line.contains("[FINE]") || + line.contains("[FINER]") || line.contains("[FINEST]")) + level = MessageLevel::Message; + if (line.contains("[SEVERE]") || line.contains("[STDERR]")) + level = MessageLevel::Error; + if (line.contains("[WARNING]")) + level = MessageLevel::Warning; + if (line.contains("[DEBUG]")) + level = MessageLevel::Debug; + } + if (line.contains("overwriting existing")) + return MessageLevel::Fatal; + //NOTE: this diverges from the real regexp. no unicode, the first section is + instead of * + static const QString javaSymbol = "([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$][a-zA-Z\\d_$]*"; + if (line.contains("Exception in thread") + || line.contains(QRegularExpression("\\s+at " + javaSymbol)) + || line.contains(QRegularExpression("Caused by: " + javaSymbol)) + || line.contains(QRegularExpression("([a-zA-Z_$][a-zA-Z\\d_$]*\\.)+[a-zA-Z_$]?[a-zA-Z\\d_$]*(Exception|Error|Throwable)")) + || line.contains(QRegularExpression("... \\d+ more$")) + ) + return MessageLevel::Error; + return level; +} + +IPathMatcher::Ptr MinecraftInstance::getLogFileMatcher() +{ + auto combined = std::make_shared(); + combined->add(std::make_shared(".*\\.log(\\.[0-9]*)?(\\.gz)?$")); + combined->add(std::make_shared("crash-.*\\.txt")); + combined->add(std::make_shared("IDMap dump.*\\.txt$")); + combined->add(std::make_shared("ModLoader\\.txt(\\..*)?$")); + return combined; +} + +QString MinecraftInstance::getLogFileRoot() +{ + return gameRoot(); +} + +QString MinecraftInstance::getStatusbarDescription() +{ + QStringList traits; + if (hasVersionBroken()) + { + traits.append(tr("broken")); + } + + QString description; + description.append(tr("Minecraft %1 (%2)").arg(m_components->getComponentVersion("net.minecraft")).arg(typeName())); + if(m_settings->get("ShowGameTime").toBool()) + { + if (lastTimePlayed() > 0) { + if (APPLICATION->settings()->get("ShowGameTimeHours").toBool()) { + description.append(tr(", last played for %1 hours").arg(Time::prettifyDurationHours(lastTimePlayed()))); + } else { + description.append(tr(", last played for %1").arg(Time::prettifyDuration(lastTimePlayed()))); + } + } + + if (totalTimePlayed() > 0) { + if (APPLICATION->settings()->get("ShowGameTimeHours").toBool()) { + description.append(tr(", total played for %1 hours").arg(Time::prettifyDurationHours(totalTimePlayed()))); + } else { + description.append(tr(", total played for %1").arg(Time::prettifyDuration(totalTimePlayed()))); + } + } + } + if(hasCrashed()) + { + description.append(tr(", has crashed.")); + } + return description; +} + +Task::Ptr MinecraftInstance::createUpdateTask(Net::Mode mode) +{ + switch (mode) + { + case Net::Mode::Offline: + { + return Task::Ptr(new MinecraftLoadAndCheck(this)); + } + case Net::Mode::Online: + { + return Task::Ptr(new MinecraftUpdate(this)); + } + } + return nullptr; +} + +shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPtr session, QuickPlayTargetPtr quickPlayTarget, quint16 localAuthServerPort) +{ + // FIXME: get rid of shared_from_this ... + auto process = LaunchTask::create(std::dynamic_pointer_cast(shared_from_this())); + auto pptr = process.get(); + + APPLICATION->icons()->saveIcon(iconKey(), FS::PathCombine(gameRoot(), "icon.png"), "PNG"); + + // print a header + { + process->appendStep(new TextPrint(pptr, "Minecraft folder is:\n" + gameRoot() + "\n\n", MessageLevel::Launcher)); + } + + // check java + { + process->appendStep(new CheckJava(pptr)); + } + + // check launch method + QStringList validMethods = {"LauncherPart", "DirectJava"}; + QString method = launchMethod(); + if(!validMethods.contains(method)) + { + process->appendStep(new TextPrint(pptr, "Selected launch method \"" + method + "\" is not valid.\n", MessageLevel::Fatal)); + return process; + } + + // create the .minecraft folder and server-resource-packs (workaround for Minecraft bug MCL-3732) + { + process->appendStep(new CreateGameFolders(pptr)); + } + + if (!quickPlayTarget && m_settings->get("JoinWorldOnLaunch").toBool()) + { + if (m_settings->get("JoinServerOnLaunch").toBool()) + { + QString fullAddress = m_settings->get("JoinServerOnLaunchAddress").toString(); + quickPlayTarget.reset(new QuickPlayTarget(QuickPlayTarget::parseMultiplayer(fullAddress))); + } + else if (m_settings->get("JoinSingleplayerWorldOnLaunch").toBool()) + { + QString worldName = m_settings->get("JoinSingleplayerWorldOnLaunchName").toString(); + quickPlayTarget.reset(new QuickPlayTarget(QuickPlayTarget::parseSingleplayer(worldName))); + } + } + + if(quickPlayTarget && quickPlayTarget->port == 25565) + { + // Resolve server address to join on launch + auto *step = new LookupServerAddress(pptr); + step->setLookupAddress(quickPlayTarget->address); + step->setOutputAddressPtr(quickPlayTarget); + process->appendStep(step); + } + + // run pre-launch command if that's needed + if(getPreLaunchCommand().size()) + { + auto step = new PreLaunchCommand(pptr); + step->setWorkingDirectory(gameRoot()); + process->appendStep(step); + } + + if(session->status != AuthSession::PlayableOffline) + { + if(!session->demo) { + process->appendStep(new ClaimAccount(pptr, session)); + } + process->appendStep(new Update(pptr, Net::Mode::Online)); + } + else + { + process->appendStep(new Update(pptr, Net::Mode::Offline)); + } + + // if there are any jar mods + { + process->appendStep(new ModMinecraftJar(pptr)); + } + + // Scan mods folders for mods + { + process->appendStep(new ScanModFolders(pptr)); + } + + // print some instance info here... + { + process->appendStep(new PrintInstanceInfo(pptr, session, quickPlayTarget)); + } + + // extract native jars if needed + { + process->appendStep(new ExtractNatives(pptr)); + } + + // reconstruct assets if needed + { + process->appendStep(new ReconstructAssets(pptr)); + } + + // verify that minimum Java requirements are met + { + process->appendStep(new VerifyJavaInstall(pptr)); + } + + auto accounts = APPLICATION->accounts(); + auto m_acct = accounts->getAccountByProfileName(session->player_name); + + // authlib patch + if (m_acct->provider()->injectorEndpoint() != "") + { + auto step = new InjectAuthlib(pptr, &m_injector); + step->setAuthServer(m_acct->provider()->injectorEndpoint().arg(localAuthServerPort)); + step->setOfflineMode(!session->wants_online); + process->appendStep(step); + } + + { + // actually launch the game + auto method = launchMethod(); + if(method == "LauncherPart") + { + auto step = new LauncherPartLaunch(pptr); + step->setWorkingDirectory(gameRoot()); + step->setAuthSession(session); + step->setQuickPlayTarget(quickPlayTarget); + process->appendStep(step); + } + else if (method == "DirectJava") + { + auto step = new DirectJavaLaunch(pptr); + step->setWorkingDirectory(gameRoot()); + step->setAuthSession(session); + step->setQuickPlayTarget(quickPlayTarget); + process->appendStep(step); + } + } + + // run post-exit command if that's needed + if(getPostExitCommand().size()) + { + auto step = new PostLaunchCommand(pptr); + step->setWorkingDirectory(gameRoot()); + process->appendStep(step); + } + if (session) + { + process->setCensorFilter(createCensorFilterFromSession(session)); + } + m_launchProcess = process; + emit launchTaskChanged(m_launchProcess); + return m_launchProcess; +} + +QString MinecraftInstance::launchMethod() +{ + return m_settings->get("MCLaunchMethod").toString(); +} + +JavaVersion MinecraftInstance::getJavaVersion() const +{ + return JavaVersion(settings()->get("JavaVersion").toString()); +} + +std::shared_ptr MinecraftInstance::loaderModList() const +{ + if (!m_loader_mod_list) + { + m_loader_mod_list.reset(new ModFolderModel(modsRoot())); + m_loader_mod_list->disableInteraction(isRunning()); + connect(this, &BaseInstance::runningStatusChanged, m_loader_mod_list.get(), &ModFolderModel::disableInteraction); + } + return m_loader_mod_list; +} + +std::shared_ptr MinecraftInstance::coreModList() const +{ + if (!m_core_mod_list) + { + m_core_mod_list.reset(new ModFolderModel(coreModsDir())); + m_core_mod_list->disableInteraction(isRunning()); + connect(this, &BaseInstance::runningStatusChanged, m_core_mod_list.get(), &ModFolderModel::disableInteraction); + } + return m_core_mod_list; +} + +std::shared_ptr MinecraftInstance::resourcePackList() const +{ + if (!m_resource_pack_list) + { + m_resource_pack_list.reset(new ResourcePackFolderModel(resourcePacksDir())); + m_resource_pack_list->disableInteraction(isRunning()); + connect(this, &BaseInstance::runningStatusChanged, m_resource_pack_list.get(), &ModFolderModel::disableInteraction); + } + return m_resource_pack_list; +} + +std::shared_ptr MinecraftInstance::texturePackList() const +{ + if (!m_texture_pack_list) + { + m_texture_pack_list.reset(new TexturePackFolderModel(texturePacksDir())); + m_texture_pack_list->disableInteraction(isRunning()); + connect(this, &BaseInstance::runningStatusChanged, m_texture_pack_list.get(), &ModFolderModel::disableInteraction); + } + return m_texture_pack_list; +} + +std::shared_ptr MinecraftInstance::shaderPackList() const +{ + if (!m_shader_pack_list) + { + m_shader_pack_list.reset(new ResourcePackFolderModel(shaderPacksDir())); + m_shader_pack_list->disableInteraction(isRunning()); + connect(this, &BaseInstance::runningStatusChanged, m_shader_pack_list.get(), &ModFolderModel::disableInteraction); + } + return m_shader_pack_list; +} + +std::shared_ptr MinecraftInstance::worldList() const +{ + if (!m_world_list) + { + m_world_list.reset(new WorldList(worldDir())); + } + return m_world_list; +} + +std::shared_ptr MinecraftInstance::gameOptionsModel() const +{ + if (!m_game_options) + { + m_game_options.reset(new GameOptions(FS::PathCombine(gameRoot(), "options.txt"))); + } + return m_game_options; +} + +QList< Mod > MinecraftInstance::getJarMods() const +{ + auto profile = m_components->getProfile(); + QList mods; + for (auto jarmod : profile->getJarMods()) + { + QStringList jar, temp1, temp2, temp3; + jarmod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, jarmodsPath().absolutePath()); + // QString filePath = jarmodsPath().absoluteFilePath(jarmod->filename(currentSystem)); + mods.push_back(Mod(QFileInfo(jar[0]))); + } + return mods; +} + + +#include "MinecraftInstance.moc" diff --git a/ultimmc/launcher/minecraft/MinecraftInstance.h b/ultimmc/launcher/minecraft/MinecraftInstance.h new file mode 100644 index 0000000..0144bc1 --- /dev/null +++ b/ultimmc/launcher/minecraft/MinecraftInstance.h @@ -0,0 +1,134 @@ +#pragma once +#include "BaseInstance.h" +#include +#include "minecraft/mod/Mod.h" +#include +#include +#include "minecraft/launch/QuickPlayTarget.h" +#include "minecraft/launch/InjectAuthlib.h" + +class ModFolderModel; +class WorldList; +class GameOptions; +class LaunchStep; +class PackProfile; + +class MinecraftInstance: public BaseInstance +{ + Q_OBJECT +public: + MinecraftInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); + virtual ~MinecraftInstance() {}; + virtual void saveNow() override; + + // FIXME: remove + QString typeName() const override; + // FIXME: remove + QSet traits() const override; + + bool canEdit() const override + { + return true; + } + + bool canExport() const override + { + return true; + } + + ////// Directories and files ////// + QString jarModsDir() const; + QString resourcePacksDir() const; + QString texturePacksDir() const; + QString shaderPacksDir() const; + QString modsRoot() const override; + QString coreModsDir() const; + QString modsCacheLocation() const; + QString libDir() const; + QString worldDir() const; + QString resourcesDir() const; + QDir jarmodsPath() const; + QDir librariesPath() const; + QDir versionsPath() const; + QString instanceConfigFolder() const override; + + // Path to the instance's minecraft directory. + QString gameRoot() const override; + + // Path to the instance's minecraft bin directory. + QString binRoot() const; + + // where to put the natives during/before launch + QString getNativePath() const; + + // where the instance-local libraries should be + QString getLocalLibraryPath() const; + + + ////// Profile management ////// + std::shared_ptr getPackProfile() const; + + ////// Mod Lists ////// + std::shared_ptr loaderModList() const; + std::shared_ptr coreModList() const; + std::shared_ptr resourcePackList() const; + std::shared_ptr texturePackList() const; + std::shared_ptr shaderPackList() const; + std::shared_ptr worldList() const; + std::shared_ptr gameOptionsModel() const; + + ////// Launch stuff ////// + Task::Ptr createUpdateTask(Net::Mode mode) override; + shared_qobject_ptr createLaunchTask(AuthSessionPtr account, QuickPlayTargetPtr quickPlayTarget, quint16 localAuthServerPort) override; + QStringList extraArguments() const override; + QStringList verboseDescription(AuthSessionPtr session, QuickPlayTargetPtr quickPlayTarget) override; + QList getJarMods() const; + QString createLaunchScript(AuthSessionPtr session, QuickPlayTargetPtr quickPlayTarget); + /// get arguments passed to java + QStringList javaArguments() const; + + /// get variables for launch command variable substitution/environment + QMap getVariables() const override; + + /// create an environment for launching processes + QProcessEnvironment createEnvironment() override; + + /// guess log level from a line of minecraft log + MessageLevel::Enum guessLevel(const QString &line, MessageLevel::Enum level) override; + + IPathMatcher::Ptr getLogFileMatcher() override; + + QString getLogFileRoot() override; + + QString getStatusbarDescription() override; + + // FIXME: remove + virtual QStringList getClassPath() const; + // FIXME: remove + virtual QStringList getNativeJars() const; + // FIXME: remove + virtual QString getMainClass() const; + + // FIXME: remove + virtual QStringList processMinecraftArgs(AuthSessionPtr account, QuickPlayTargetPtr quickPlayTarget) const; + + virtual JavaVersion getJavaVersion() const; + +protected: + QMap createCensorFilterFromSession(AuthSessionPtr session); + QStringList validLaunchMethods(); + QString launchMethod(); + +protected: // data + std::shared_ptr m_components; + mutable std::shared_ptr m_loader_mod_list; + mutable std::shared_ptr m_core_mod_list; + mutable std::shared_ptr m_resource_pack_list; + mutable std::shared_ptr m_shader_pack_list; + mutable std::shared_ptr m_texture_pack_list; + mutable std::shared_ptr m_world_list; + mutable std::shared_ptr m_game_options; + mutable std::shared_ptr m_injector; +}; + +typedef std::shared_ptr MinecraftInstancePtr; diff --git a/ultimmc/launcher/minecraft/MinecraftLoadAndCheck.cpp b/ultimmc/launcher/minecraft/MinecraftLoadAndCheck.cpp new file mode 100644 index 0000000..79b0c48 --- /dev/null +++ b/ultimmc/launcher/minecraft/MinecraftLoadAndCheck.cpp @@ -0,0 +1,45 @@ +#include "MinecraftLoadAndCheck.h" +#include "MinecraftInstance.h" +#include "PackProfile.h" + +MinecraftLoadAndCheck::MinecraftLoadAndCheck(MinecraftInstance *inst, QObject *parent) : Task(parent), m_inst(inst) +{ +} + +void MinecraftLoadAndCheck::executeTask() +{ + // add offline metadata load task + auto components = m_inst->getPackProfile(); + components->reload(Net::Mode::Offline); + m_task = components->getCurrentTask(); + + if(!m_task) + { + emitSucceeded(); + return; + } + connect(m_task.get(), &Task::succeeded, this, &MinecraftLoadAndCheck::subtaskSucceeded); + connect(m_task.get(), &Task::failed, this, &MinecraftLoadAndCheck::subtaskFailed); + connect(m_task.get(), &Task::progress, this, &MinecraftLoadAndCheck::progress); + connect(m_task.get(), &Task::status, this, &MinecraftLoadAndCheck::setStatus); +} + +void MinecraftLoadAndCheck::subtaskSucceeded() +{ + if(isFinished()) + { + qCritical() << "MinecraftUpdate: Subtask" << sender() << "succeeded, but work was already done!"; + return; + } + emitSucceeded(); +} + +void MinecraftLoadAndCheck::subtaskFailed(QString error) +{ + if(isFinished()) + { + qCritical() << "MinecraftUpdate: Subtask" << sender() << "failed, but work was already done!"; + return; + } + emitFailed(error); +} diff --git a/ultimmc/launcher/minecraft/MinecraftLoadAndCheck.h b/ultimmc/launcher/minecraft/MinecraftLoadAndCheck.h new file mode 100644 index 0000000..bfeae46 --- /dev/null +++ b/ultimmc/launcher/minecraft/MinecraftLoadAndCheck.h @@ -0,0 +1,48 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "tasks/Task.h" +#include + +#include "QObjectPtr.h" + +class MinecraftVersion; +class MinecraftInstance; + +class MinecraftLoadAndCheck : public Task +{ + Q_OBJECT +public: + explicit MinecraftLoadAndCheck(MinecraftInstance *inst, QObject *parent = 0); + virtual ~MinecraftLoadAndCheck() {}; + void executeTask() override; + +private slots: + void subtaskSucceeded(); + void subtaskFailed(QString error); + +private: + MinecraftInstance *m_inst = nullptr; + Task::Ptr m_task; + QString m_preFailure; + QString m_fail_reason; +}; + diff --git a/ultimmc/launcher/minecraft/MinecraftUpdate.cpp b/ultimmc/launcher/minecraft/MinecraftUpdate.cpp new file mode 100644 index 0000000..32e9cbb --- /dev/null +++ b/ultimmc/launcher/minecraft/MinecraftUpdate.cpp @@ -0,0 +1,181 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MinecraftUpdate.h" +#include "MinecraftInstance.h" + +#include +#include +#include +#include + +#include "BaseInstance.h" +#include "minecraft/PackProfile.h" +#include "minecraft/Library.h" +#include + +#include "update/FoldersTask.h" +#include "update/LibrariesTask.h" +#include "update/FMLLibrariesTask.h" +#include "update/AssetUpdateTask.h" + +#include +#include + +MinecraftUpdate::MinecraftUpdate(MinecraftInstance *inst, QObject *parent) : Task(parent), m_inst(inst) +{ +} + +void MinecraftUpdate::executeTask() +{ + m_tasks.clear(); + // create folders + { + m_tasks.append(std::make_shared(m_inst)); + } + + // add metadata update task if necessary + { + auto components = m_inst->getPackProfile(); + components->reload(Net::Mode::Online); + auto task = components->getCurrentTask(); + if(task) + { + m_tasks.append(task.unwrap()); + } + } + + // libraries download + { + m_tasks.append(std::make_shared(m_inst)); + } + + // FML libraries download and copy into the instance + { + m_tasks.append(std::make_shared(m_inst)); + } + + // assets update + { + m_tasks.append(std::make_shared(m_inst)); + } + + if(!m_preFailure.isEmpty()) + { + emitFailed(m_preFailure); + return; + } + next(); +} + +void MinecraftUpdate::next() +{ + if(m_abort) + { + emitFailed(tr("Aborted by user.")); + return; + } + if(m_failed_out_of_order) + { + emitFailed(m_fail_reason); + return; + } + m_currentTask ++; + if(m_currentTask > 0) + { + auto task = m_tasks[m_currentTask - 1]; + disconnect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded); + disconnect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); + disconnect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); + disconnect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); + } + if(m_currentTask == m_tasks.size()) + { + emitSucceeded(); + return; + } + auto task = m_tasks[m_currentTask]; + // if the task is already finished by the time we look at it, skip it + if(task->isFinished()) + { + qCritical() << "MinecraftUpdate: Skipping finished subtask" << m_currentTask << ":" << task.get(); + next(); + } + connect(task.get(), &Task::succeeded, this, &MinecraftUpdate::subtaskSucceeded); + connect(task.get(), &Task::failed, this, &MinecraftUpdate::subtaskFailed); + connect(task.get(), &Task::progress, this, &MinecraftUpdate::progress); + connect(task.get(), &Task::status, this, &MinecraftUpdate::setStatus); + // if the task is already running, do not start it again + if(!task->isRunning()) + { + task->start(); + } +} + +void MinecraftUpdate::subtaskSucceeded() +{ + if(isFinished()) + { + qCritical() << "MinecraftUpdate: Subtask" << sender() << "succeeded, but work was already done!"; + return; + } + auto senderTask = QObject::sender(); + auto currentTask = m_tasks[m_currentTask].get(); + if(senderTask != currentTask) + { + qDebug() << "MinecraftUpdate: Subtask" << sender() << "succeeded out of order."; + return; + } + next(); +} + +void MinecraftUpdate::subtaskFailed(QString error) +{ + if(isFinished()) + { + qCritical() << "MinecraftUpdate: Subtask" << sender() << "failed, but work was already done!"; + return; + } + auto senderTask = QObject::sender(); + auto currentTask = m_tasks[m_currentTask].get(); + if(senderTask != currentTask) + { + qDebug() << "MinecraftUpdate: Subtask" << sender() << "failed out of order."; + m_failed_out_of_order = true; + m_fail_reason = error; + return; + } + emitFailed(error); +} + + +bool MinecraftUpdate::abort() +{ + if(!m_abort) + { + m_abort = true; + auto task = m_tasks[m_currentTask]; + if(task->canAbort()) + { + return task->abort(); + } + } + return true; +} + +bool MinecraftUpdate::canAbort() const +{ + return true; +} diff --git a/ultimmc/launcher/minecraft/MinecraftUpdate.h b/ultimmc/launcher/minecraft/MinecraftUpdate.h new file mode 100644 index 0000000..fadebff --- /dev/null +++ b/ultimmc/launcher/minecraft/MinecraftUpdate.h @@ -0,0 +1,57 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "net/NetJob.h" +#include "tasks/Task.h" +#include "minecraft/VersionFilterData.h" +#include + +class MinecraftVersion; +class MinecraftInstance; + +class MinecraftUpdate : public Task +{ + Q_OBJECT +public: + explicit MinecraftUpdate(MinecraftInstance *inst, QObject *parent = 0); + virtual ~MinecraftUpdate() {}; + + void executeTask() override; + bool canAbort() const override; + +private +slots: + bool abort() override; + void subtaskSucceeded(); + void subtaskFailed(QString error); + +private: + void next(); + +private: + MinecraftInstance *m_inst = nullptr; + QList> m_tasks; + QString m_preFailure; + int m_currentTask = -1; + bool m_abort = false; + bool m_failed_out_of_order = false; + QString m_fail_reason; +}; diff --git a/ultimmc/launcher/minecraft/MojangDownloadInfo.h b/ultimmc/launcher/minecraft/MojangDownloadInfo.h new file mode 100644 index 0000000..88f8728 --- /dev/null +++ b/ultimmc/launcher/minecraft/MojangDownloadInfo.h @@ -0,0 +1,82 @@ +#pragma once +#include +#include +#include + +struct MojangDownloadInfo +{ + // types + typedef std::shared_ptr Ptr; + + // data + /// Local filesystem path. WARNING: not used, only here so we can pass through mojang files unmolested! + QString path; + /// absolute URL of this file + QString url; + /// sha-1 checksum of the file + QString sha1; + /// size of the file in bytes + int size; +}; + + + +struct MojangLibraryDownloadInfo +{ + MojangLibraryDownloadInfo(MojangDownloadInfo::Ptr artifact): artifact(artifact) {}; + MojangLibraryDownloadInfo() {}; + + // types + typedef std::shared_ptr Ptr; + + // methods + MojangDownloadInfo *getDownloadInfo(QString classifier) + { + if (classifier.isNull()) + { + return artifact.get(); + } + + return classifiers[classifier].get(); + } + + // data + MojangDownloadInfo::Ptr artifact; + QMap classifiers; +}; + + + +struct MojangAssetIndexInfo : public MojangDownloadInfo +{ + // types + typedef std::shared_ptr Ptr; + + // methods + MojangAssetIndexInfo() + { + } + + MojangAssetIndexInfo(QString id) + { + this->id = id; + // HACK: ignore assets from other version files than Minecraft + // workaround for stupid assets issue caused by amazon: + // https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire/ + if(id == "legacy") + { + url = "https://launchermeta.mojang.com/mc/assets/legacy/c0fd82e8ce9fbc93119e40d96d5a4e62cfa3f729/legacy.json"; + } + // HACK + else + { + url = "https://s3.amazonaws.com/Minecraft.Download/indexes/" + id + ".json"; + } + known = false; + } + + // data + int totalSize; + QString id; + bool known = true; +}; diff --git a/ultimmc/launcher/minecraft/MojangVersionFormat.cpp b/ultimmc/launcher/minecraft/MojangVersionFormat.cpp new file mode 100644 index 0000000..58c15f5 --- /dev/null +++ b/ultimmc/launcher/minecraft/MojangVersionFormat.cpp @@ -0,0 +1,386 @@ +#include "MojangVersionFormat.h" +#include "OneSixVersionFormat.h" +#include "MojangDownloadInfo.h" + +#include "Json.h" +using namespace Json; +#include "ParseUtils.h" +#include + +static const int CURRENT_MINIMUM_LAUNCHER_VERSION = 18; + +static MojangAssetIndexInfo::Ptr assetIndexFromJson (const QJsonObject &obj); +static MojangDownloadInfo::Ptr downloadInfoFromJson (const QJsonObject &obj); +static MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson (const QJsonObject &libObj); +static QJsonObject assetIndexToJson (MojangAssetIndexInfo::Ptr assetidxinfo); +static QJsonObject libDownloadInfoToJson (MojangLibraryDownloadInfo::Ptr libinfo); +static QJsonObject downloadInfoToJson (MojangDownloadInfo::Ptr info); + +namespace Bits +{ +static void readString(const QJsonObject &root, const QString &key, QString &variable) +{ + if (root.contains(key)) + { + variable = requireValueString(root.value(key)); + } +} + +static void readDownloadInfo(MojangDownloadInfo::Ptr out, const QJsonObject &obj) +{ + // optional, not used + readString(obj, "path", out->path); + // required! + out->sha1 = requireString(obj, "sha1"); + out->url = requireString(obj, "url"); + out->size = requireInteger(obj, "size"); +} + +static void readAssetIndex(MojangAssetIndexInfo::Ptr out, const QJsonObject &obj) +{ + out->totalSize = requireInteger(obj, "totalSize"); + out->id = requireString(obj, "id"); + // out->known = true; +} +} + +MojangDownloadInfo::Ptr downloadInfoFromJson(const QJsonObject &obj) +{ + auto out = std::make_shared(); + Bits::readDownloadInfo(out, obj); + return out; +} + +MojangAssetIndexInfo::Ptr assetIndexFromJson(const QJsonObject &obj) +{ + auto out = std::make_shared(); + Bits::readDownloadInfo(out, obj); + Bits::readAssetIndex(out, obj); + return out; +} + +QJsonObject downloadInfoToJson(MojangDownloadInfo::Ptr info) +{ + QJsonObject out; + if(!info->path.isNull()) + { + out.insert("path", info->path); + } + out.insert("sha1", info->sha1); + out.insert("size", info->size); + out.insert("url", info->url); + return out; +} + +MojangLibraryDownloadInfo::Ptr libDownloadInfoFromJson(const QJsonObject &libObj) +{ + auto out = std::make_shared(); + auto dlObj = requireValueObject(libObj.value("downloads")); + if(dlObj.contains("artifact")) + { + out->artifact = downloadInfoFromJson(requireObject(dlObj, "artifact")); + } + if(dlObj.contains("classifiers")) + { + auto classifiersObj = requireObject(dlObj, "classifiers"); + for(auto iter = classifiersObj.begin(); iter != classifiersObj.end(); iter++) + { + auto classifier = iter.key(); + auto classifierObj = requireValueObject(iter.value()); + out->classifiers[classifier] = downloadInfoFromJson(classifierObj); + } + } + return out; +} + +QJsonObject libDownloadInfoToJson(MojangLibraryDownloadInfo::Ptr libinfo) +{ + QJsonObject out; + if(libinfo->artifact) + { + out.insert("artifact", downloadInfoToJson(libinfo->artifact)); + } + if(libinfo->classifiers.size()) + { + QJsonObject classifiersOut; + for(auto iter = libinfo->classifiers.begin(); iter != libinfo->classifiers.end(); iter++) + { + classifiersOut.insert(iter.key(), downloadInfoToJson(iter.value())); + } + out.insert("classifiers", classifiersOut); + } + return out; +} + +QJsonObject assetIndexToJson(MojangAssetIndexInfo::Ptr info) +{ + QJsonObject out; + if(!info->path.isNull()) + { + out.insert("path", info->path); + } + out.insert("sha1", info->sha1); + out.insert("size", info->size); + out.insert("url", info->url); + out.insert("totalSize", info->totalSize); + out.insert("id", info->id); + return out; +} + +void MojangVersionFormat::readVersionProperties(const QJsonObject &in, VersionFile *out) +{ + Bits::readString(in, "id", out->minecraftVersion); + Bits::readString(in, "mainClass", out->mainClass); + Bits::readString(in, "minecraftArguments", out->minecraftArguments); + if(out->minecraftArguments.isEmpty()) + { + QString processArguments; + Bits::readString(in, "processArguments", processArguments); + QString toCompare = processArguments.toLower(); + if (toCompare == "legacy") + { + out->minecraftArguments = " ${auth_player_name} ${auth_session}"; + } + else if (toCompare == "username_session") + { + out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session}"; + } + else if (toCompare == "username_session_version") + { + out->minecraftArguments = "--username ${auth_player_name} --session ${auth_session} --version ${profile_name}"; + } + else if (!toCompare.isEmpty()) + { + out->addProblem(ProblemSeverity::Error, QObject::tr("processArguments is set to unknown value '%1'").arg(processArguments)); + } + } + Bits::readString(in, "type", out->type); + + Bits::readString(in, "assets", out->assets); + if(in.contains("assetIndex")) + { + out->mojangAssetIndex = assetIndexFromJson(requireObject(in, "assetIndex")); + } + else if (!out->assets.isNull()) + { + out->mojangAssetIndex = std::make_shared(out->assets); + } + + out->releaseTime = timeFromS3Time(in.value("releaseTime").toString("")); + out->updateTime = timeFromS3Time(in.value("time").toString("")); + + if (in.contains("minimumLauncherVersion")) + { + out->minimumLauncherVersion = requireValueInteger(in.value("minimumLauncherVersion")); + if (out->minimumLauncherVersion > CURRENT_MINIMUM_LAUNCHER_VERSION) + { + out->addProblem( + ProblemSeverity::Warning, + QObject::tr("The 'minimumLauncherVersion' value of this version (%1) is higher than supported by %3 (%2). It might not work properly!") + .arg(out->minimumLauncherVersion) + .arg(CURRENT_MINIMUM_LAUNCHER_VERSION) + .arg(BuildConfig.LAUNCHER_NAME) + ); + } + } + if(in.contains("downloads")) + { + auto downloadsObj = requireObject(in, "downloads"); + for(auto iter = downloadsObj.begin(); iter != downloadsObj.end(); iter++) + { + auto classifier = iter.key(); + auto classifierObj = requireValueObject(iter.value()); + out->mojangDownloads[classifier] = downloadInfoFromJson(classifierObj); + } + } +} + +VersionFilePtr MojangVersionFormat::versionFileFromJson(const QJsonDocument &doc, const QString &filename) +{ + VersionFilePtr out(new VersionFile()); + if (doc.isEmpty() || doc.isNull()) + { + throw JSONValidationError(filename + " is empty or null"); + } + if (!doc.isObject()) + { + throw JSONValidationError(filename + " is not an object"); + } + + QJsonObject root = doc.object(); + + readVersionProperties(root, out.get()); + + out->name = "Minecraft"; + out->uid = "net.minecraft"; + out->version = out->minecraftVersion; + // out->filename = filename; + + + if (root.contains("libraries")) + { + for (auto libVal : requireValueArray(root.value("libraries"))) + { + auto libObj = requireValueObject(libVal); + + auto lib = MojangVersionFormat::libraryFromJson(*out, libObj, filename); + out->libraries.append(lib); + } + } + return out; +} + +void MojangVersionFormat::writeVersionProperties(const VersionFile* in, QJsonObject& out) +{ + writeString(out, "id", in->minecraftVersion); + writeString(out, "mainClass", in->mainClass); + writeString(out, "minecraftArguments", in->minecraftArguments); + writeString(out, "type", in->type); + if(!in->releaseTime.isNull()) + { + writeString(out, "releaseTime", timeToS3Time(in->releaseTime)); + } + if(!in->updateTime.isNull()) + { + writeString(out, "time", timeToS3Time(in->updateTime)); + } + if(in->minimumLauncherVersion != -1) + { + out.insert("minimumLauncherVersion", in->minimumLauncherVersion); + } + writeString(out, "assets", in->assets); + if(in->mojangAssetIndex && in->mojangAssetIndex->known) + { + out.insert("assetIndex", assetIndexToJson(in->mojangAssetIndex)); + } + if(in->mojangDownloads.size()) + { + QJsonObject downloadsOut; + for(auto iter = in->mojangDownloads.begin(); iter != in->mojangDownloads.end(); iter++) + { + downloadsOut.insert(iter.key(), downloadInfoToJson(iter.value())); + } + out.insert("downloads", downloadsOut); + } +} + +QJsonDocument MojangVersionFormat::versionFileToJson(const VersionFilePtr &patch) +{ + QJsonObject root; + writeVersionProperties(patch.get(), root); + if (!patch->libraries.isEmpty()) + { + QJsonArray array; + for (auto value: patch->libraries) + { + array.append(MojangVersionFormat::libraryToJson(value.get())); + } + root.insert("libraries", array); + } + + // write the contents to a json document. + { + QJsonDocument out; + out.setObject(root); + return out; + } +} + +LibraryPtr MojangVersionFormat::libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename) +{ + LibraryPtr out(new Library()); + if (!libObj.contains("name")) + { + throw JSONValidationError(filename + "contains a library that doesn't have a 'name' field"); + } + auto rawName = libObj.value("name").toString(); + out->m_name = rawName; + if(!out->m_name.valid()) { + problems.addProblem(ProblemSeverity::Error, QObject::tr("Library %1 name is broken and cannot be processed.").arg(rawName)); + } + + Bits::readString(libObj, "url", out->m_repositoryURL); + if (libObj.contains("extract")) + { + out->m_hasExcludes = true; + auto extractObj = requireValueObject(libObj.value("extract")); + for (auto excludeVal : requireValueArray(extractObj.value("exclude"))) + { + out->m_extractExcludes.append(requireValueString(excludeVal)); + } + } + if (libObj.contains("natives")) + { + QJsonObject nativesObj = requireValueObject(libObj.value("natives")); + for (auto it = nativesObj.begin(); it != nativesObj.end(); ++it) + { + if (!it.value().isString()) + { + qWarning() << filename << "contains an invalid native (skipping)"; + } + OpSys opSys = OpSys_fromString(it.key()); + if (opSys != Os_Other) + { + out->m_nativeClassifiers[opSys] = it.value().toString(); + } + } + } + if (libObj.contains("rules")) + { + out->applyRules = true; + out->m_rules = rulesFromJsonV4(libObj); + } + if (libObj.contains("downloads")) + { + out->m_mojangDownloads = libDownloadInfoFromJson(libObj); + } + return out; +} + +QJsonObject MojangVersionFormat::libraryToJson(Library *library) +{ + QJsonObject libRoot; + libRoot.insert("name", library->m_name.serialize()); + if (!library->m_repositoryURL.isEmpty()) + { + libRoot.insert("url", library->m_repositoryURL); + } + if (library->isNative()) + { + QJsonObject nativeList; + auto iter = library->m_nativeClassifiers.begin(); + while (iter != library->m_nativeClassifiers.end()) + { + nativeList.insert(OpSys_toString(iter.key()), iter.value()); + iter++; + } + libRoot.insert("natives", nativeList); + if (library->m_extractExcludes.size()) + { + QJsonArray excludes; + QJsonObject extract; + for (auto exclude : library->m_extractExcludes) + { + excludes.append(exclude); + } + extract.insert("exclude", excludes); + libRoot.insert("extract", extract); + } + } + if (library->m_rules.size()) + { + QJsonArray allRules; + for (auto &rule : library->m_rules) + { + QJsonObject ruleObj = rule->toJson(); + allRules.append(ruleObj); + } + libRoot.insert("rules", allRules); + } + if(library->m_mojangDownloads) + { + auto downloadsObj = libDownloadInfoToJson(library->m_mojangDownloads); + libRoot.insert("downloads", downloadsObj); + } + return libRoot; +} diff --git a/ultimmc/launcher/minecraft/MojangVersionFormat.h b/ultimmc/launcher/minecraft/MojangVersionFormat.h new file mode 100644 index 0000000..d38f0a2 --- /dev/null +++ b/ultimmc/launcher/minecraft/MojangVersionFormat.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include +#include + +class MojangVersionFormat +{ +friend class OneSixVersionFormat; +protected: + // does not include libraries + static void readVersionProperties(const QJsonObject& in, VersionFile* out); + // does not include libraries + static void writeVersionProperties(const VersionFile* in, QJsonObject& out); +public: + // version files / profile patches + static VersionFilePtr versionFileFromJson(const QJsonDocument &doc, const QString &filename); + static QJsonDocument versionFileToJson(const VersionFilePtr &patch); + + // libraries + static LibraryPtr libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); + static QJsonObject libraryToJson(Library *library); +}; diff --git a/ultimmc/launcher/minecraft/MojangVersionFormat_test.cpp b/ultimmc/launcher/minecraft/MojangVersionFormat_test.cpp new file mode 100644 index 0000000..9d09534 --- /dev/null +++ b/ultimmc/launcher/minecraft/MojangVersionFormat_test.cpp @@ -0,0 +1,55 @@ +#include +#include +#include "TestUtil.h" + +#include "minecraft/MojangVersionFormat.h" + +class MojangVersionFormatTest : public QObject +{ + Q_OBJECT + + static QJsonDocument readJson(const char *file) + { + auto path = QFINDTESTDATA(file); + QFile jsonFile(path); + jsonFile.open(QIODevice::ReadOnly); + auto data = jsonFile.readAll(); + jsonFile.close(); + return QJsonDocument::fromJson(data); + } + static void writeJson(const char *file, QJsonDocument doc) + { + QFile jsonFile(file); + jsonFile.open(QIODevice::WriteOnly | QIODevice::Text); + auto data = doc.toJson(QJsonDocument::Indented); + qDebug() << QString::fromUtf8(data); + jsonFile.write(data); + jsonFile.close(); + } + +private +slots: + void test_Through_Simple() + { + QJsonDocument doc = readJson("data/1.9-simple.json"); + auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9-simple.json"); + auto doc2 = MojangVersionFormat::versionFileToJson(vfile); + writeJson("1.9-simple-passthorugh.json", doc2); + + QCOMPARE(doc.toJson(), doc2.toJson()); + } + + void test_Through() + { + QJsonDocument doc = readJson("data/1.9.json"); + auto vfile = MojangVersionFormat::versionFileFromJson(doc, "1.9.json"); + auto doc2 = MojangVersionFormat::versionFileToJson(vfile); + writeJson("1.9-passthorugh.json", doc2); + QCOMPARE(doc.toJson(), doc2.toJson()); + } +}; + +QTEST_GUILESS_MAIN(MojangVersionFormatTest) + +#include "MojangVersionFormat_test.moc" + diff --git a/ultimmc/launcher/minecraft/OneSixVersionFormat.cpp b/ultimmc/launcher/minecraft/OneSixVersionFormat.cpp new file mode 100644 index 0000000..26b5769 --- /dev/null +++ b/ultimmc/launcher/minecraft/OneSixVersionFormat.cpp @@ -0,0 +1,391 @@ +#include "OneSixVersionFormat.h" +#include +#include "minecraft/ParseUtils.h" +#include + +using namespace Json; + +static void readString(const QJsonObject &root, const QString &key, QString &variable) +{ + if (root.contains(key)) + { + variable = requireValueString(root.value(key)); + } +} + +LibraryPtr OneSixVersionFormat::libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename) +{ + LibraryPtr out = MojangVersionFormat::libraryFromJson(problems, libObj, filename); + readString(libObj, "MMC-hint", out->m_hint); + readString(libObj, "MMC-absulute_url", out->m_absoluteURL); + readString(libObj, "MMC-absoluteUrl", out->m_absoluteURL); + readString(libObj, "MMC-filename", out->m_filename); + readString(libObj, "MMC-displayname", out->m_displayname); + return out; +} + +QJsonObject OneSixVersionFormat::libraryToJson(Library *library) +{ + QJsonObject libRoot = MojangVersionFormat::libraryToJson(library); + if (library->m_absoluteURL.size()) + libRoot.insert("MMC-absoluteUrl", library->m_absoluteURL); + if (library->m_hint.size()) + libRoot.insert("MMC-hint", library->m_hint); + if (library->m_filename.size()) + libRoot.insert("MMC-filename", library->m_filename); + if (library->m_displayname.size()) + libRoot.insert("MMC-displayname", library->m_displayname); + return libRoot; +} + +VersionFilePtr OneSixVersionFormat::versionFileFromJson(const QJsonDocument &doc, const QString &filename, const bool requireOrder) +{ + VersionFilePtr out(new VersionFile()); + if (doc.isEmpty() || doc.isNull()) + { + throw JSONValidationError(filename + " is empty or null"); + } + if (!doc.isObject()) + { + throw JSONValidationError(filename + " is not an object"); + } + + QJsonObject root = doc.object(); + + Meta::MetadataVersion formatVersion = Meta::parseFormatVersion(root, false); + switch(formatVersion) + { + case Meta::MetadataVersion::InitialRelease: + break; + case Meta::MetadataVersion::Invalid: + throw JSONValidationError(filename + " does not contain a recognizable version of the metadata format."); + } + + if (requireOrder) + { + if (root.contains("order")) + { + out->order = requireValueInteger(root.value("order")); + } + else + { + // FIXME: evaluate if we don't want to throw exceptions here instead + qCritical() << filename << "doesn't contain an order field"; + } + } + + out->name = root.value("name").toString(); + + if(root.contains("uid")) + { + out->uid = root.value("uid").toString(); + } + else + { + out->uid = root.value("fileId").toString(); + } + + out->version = root.value("version").toString(); + + MojangVersionFormat::readVersionProperties(root, out.get()); + + // added for legacy Minecraft window embedding, TODO: remove + readString(root, "appletClass", out->appletClass); + + if (root.contains("+tweakers")) + { + for (auto tweakerVal : requireValueArray(root.value("+tweakers"))) + { + out->addTweakers.append(requireValueString(tweakerVal)); + } + } + + if (root.contains("+traits")) + { + for (auto tweakerVal : requireValueArray(root.value("+traits"))) + { + out->traits.insert(requireValueString(tweakerVal)); + } + } + + + if (root.contains("jarMods")) + { + for (auto libVal : requireValueArray(root.value("jarMods"))) + { + QJsonObject libObj = requireValueObject(libVal); + // parse the jarmod + auto lib = OneSixVersionFormat::jarModFromJson(*out, libObj, filename); + // and add to jar mods + out->jarMods.append(lib); + } + } + else if (root.contains("+jarMods")) // DEPRECATED: old style '+jarMods' are only here for backwards compatibility + { + for (auto libVal : requireValueArray(root.value("+jarMods"))) + { + QJsonObject libObj = requireValueObject(libVal); + // parse the jarmod + auto lib = OneSixVersionFormat::plusJarModFromJson(*out, libObj, filename, out->name); + // and add to jar mods + out->jarMods.append(lib); + } + } + + if (root.contains("mods")) + { + for (auto libVal : requireValueArray(root.value("mods"))) + { + QJsonObject libObj = requireValueObject(libVal); + // parse the jarmod + auto lib = OneSixVersionFormat::modFromJson(*out, libObj, filename); + // and add to jar mods + out->mods.append(lib); + } + } + + auto readLibs = [&](const char * which, QList & outList) + { + for (auto libVal : requireValueArray(root.value(which))) + { + QJsonObject libObj = requireValueObject(libVal); + // parse the library + auto lib = libraryFromJson(*out, libObj, filename); + outList.append(lib); + } + }; + bool hasPlusLibs = root.contains("+libraries"); + bool hasLibs = root.contains("libraries"); + if (hasPlusLibs && hasLibs) + { + out->addProblem(ProblemSeverity::Warning, + QObject::tr("Version file has both '+libraries' and 'libraries'. This is no longer supported.")); + readLibs("libraries", out->libraries); + readLibs("+libraries", out->libraries); + } + else if (hasLibs) + { + readLibs("libraries", out->libraries); + } + else if(hasPlusLibs) + { + readLibs("+libraries", out->libraries); + } + + if(root.contains("mavenFiles")) { + readLibs("mavenFiles", out->mavenFiles); + } + + // if we have mainJar, just use it + if(root.contains("mainJar")) + { + QJsonObject libObj = requireObject(root, "mainJar"); + out->mainJar = libraryFromJson(*out, libObj, filename); + } + // else reconstruct it from downloads and id ... if that's available + else if(!out->minecraftVersion.isEmpty()) + { + auto lib = std::make_shared(); + lib->setRawName(GradleSpecifier(QString("com.mojang:minecraft:%1:client").arg(out->minecraftVersion))); + // we have a reliable client download, use it. + if(out->mojangDownloads.contains("client")) + { + auto LibDLInfo = std::make_shared(); + LibDLInfo->artifact = out->mojangDownloads["client"]; + lib->setMojangDownloadInfo(LibDLInfo); + } + // we got nothing... + else + { + out->addProblem( + ProblemSeverity::Error, + QObject::tr("URL for the main jar could not be determined - Mojang removed the server that we used as fallback.") + ); + } + out->mainJar = lib; + } + + if (root.contains("requires")) + { + Meta::parseRequires(root, &out->depends); + } + QString dependsOnMinecraftVersion = root.value("mcVersion").toString(); + if(!dependsOnMinecraftVersion.isEmpty()) + { + Meta::Require mcReq; + mcReq.uid = "net.minecraft"; + mcReq.equalsVersion = dependsOnMinecraftVersion; + if (out->depends.count(mcReq) == 0) + { + out->depends.insert(mcReq); + } + } + if (root.contains("conflicts")) + { + Meta::parseRequires(root, &out->conflicts); + } + if (root.contains("volatile")) + { + out->m_volatile = requireBoolean(root, "volatile"); + } + + /* removed features that shouldn't be used */ + if (root.contains("tweakers")) + { + out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element 'tweakers'")); + } + if (root.contains("-libraries")) + { + out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-libraries'")); + } + if (root.contains("-tweakers")) + { + out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-tweakers'")); + } + if (root.contains("-minecraftArguments")) + { + out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '-minecraftArguments'")); + } + if (root.contains("+minecraftArguments")) + { + out->addProblem(ProblemSeverity::Error, QObject::tr("Version file contains unsupported element '+minecraftArguments'")); + } + return out; +} + +QJsonDocument OneSixVersionFormat::versionFileToJson(const VersionFilePtr &patch) +{ + QJsonObject root; + writeString(root, "name", patch->name); + + writeString(root, "uid", patch->uid); + + writeString(root, "version", patch->version); + + Meta::serializeFormatVersion(root, Meta::MetadataVersion::InitialRelease); + + MojangVersionFormat::writeVersionProperties(patch.get(), root); + + if(patch->mainJar) + { + root.insert("mainJar", libraryToJson(patch->mainJar.get())); + } + writeString(root, "appletClass", patch->appletClass); + writeStringList(root, "+tweakers", patch->addTweakers); + writeStringList(root, "+traits", patch->traits.toList()); + if (!patch->libraries.isEmpty()) + { + QJsonArray array; + for (auto value: patch->libraries) + { + array.append(OneSixVersionFormat::libraryToJson(value.get())); + } + root.insert("libraries", array); + } + if (!patch->mavenFiles.isEmpty()) + { + QJsonArray array; + for (auto value: patch->mavenFiles) + { + array.append(OneSixVersionFormat::libraryToJson(value.get())); + } + root.insert("mavenFiles", array); + } + if (!patch->jarMods.isEmpty()) + { + QJsonArray array; + for (auto value: patch->jarMods) + { + array.append(OneSixVersionFormat::jarModtoJson(value.get())); + } + root.insert("jarMods", array); + } + if (!patch->mods.isEmpty()) + { + QJsonArray array; + for (auto value: patch->jarMods) + { + array.append(OneSixVersionFormat::modtoJson(value.get())); + } + root.insert("mods", array); + } + if(!patch->depends.empty()) + { + Meta::serializeRequires(root, &patch->depends, "requires"); + } + if(!patch->conflicts.empty()) + { + Meta::serializeRequires(root, &patch->conflicts, "conflicts"); + } + if(patch->m_volatile) + { + root.insert("volatile", true); + } + // write the contents to a json document. + { + QJsonDocument out; + out.setObject(root); + return out; + } +} + +LibraryPtr OneSixVersionFormat::plusJarModFromJson( + ProblemContainer & problems, + const QJsonObject &libObj, + const QString &filename, + const QString &originalName +) { + LibraryPtr out(new Library()); + if (!libObj.contains("name")) + { + throw JSONValidationError(filename + + "contains a jarmod that doesn't have a 'name' field"); + } + + // just make up something unique on the spot for the library name. + auto uuid = QUuid::createUuid(); + QString id = uuid.toString().remove('{').remove('}'); + out->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1")); + + // filename override is the old name + out->setFilename(libObj.value("name").toString()); + + // it needs to be local, it is stored in the instance jarmods folder + out->setHint("local"); + + // read the original name if present - some versions did not set it + // it is the original jar mod filename before it got renamed at the point of addition + auto displayName = libObj.value("originalName").toString(); + if(displayName.isEmpty()) + { + auto fixed = originalName; + fixed.remove(" (jar mod)"); + out->setDisplayName(fixed); + } + else + { + out->setDisplayName(displayName); + } + return out; +} + +LibraryPtr OneSixVersionFormat::jarModFromJson(ProblemContainer & problems, const QJsonObject& libObj, const QString& filename) +{ + return libraryFromJson(problems, libObj, filename); +} + + +QJsonObject OneSixVersionFormat::jarModtoJson(Library *jarmod) +{ + return libraryToJson(jarmod); +} + +LibraryPtr OneSixVersionFormat::modFromJson(ProblemContainer & problems, const QJsonObject& libObj, const QString& filename) +{ + return libraryFromJson(problems, libObj, filename); +} + +QJsonObject OneSixVersionFormat::modtoJson(Library *jarmod) +{ + return libraryToJson(jarmod); +} diff --git a/ultimmc/launcher/minecraft/OneSixVersionFormat.h b/ultimmc/launcher/minecraft/OneSixVersionFormat.h new file mode 100644 index 0000000..1a091d8 --- /dev/null +++ b/ultimmc/launcher/minecraft/OneSixVersionFormat.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include +#include + +class OneSixVersionFormat +{ +public: + // version files / profile patches + static VersionFilePtr versionFileFromJson(const QJsonDocument &doc, const QString &filename, const bool requireOrder); + static QJsonDocument versionFileToJson(const VersionFilePtr &patch); + + // libraries + static LibraryPtr libraryFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); + static QJsonObject libraryToJson(Library *library); + + // DEPRECATED: old 'plus' jar mods generated by the application + static LibraryPtr plusJarModFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename, const QString &originalName); + + // new jar mods derived from libraries + static LibraryPtr jarModFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); + static QJsonObject jarModtoJson(Library * jarmod); + + // mods, also derived from libraries + static LibraryPtr modFromJson(ProblemContainer & problems, const QJsonObject &libObj, const QString &filename); + static QJsonObject modtoJson(Library * jarmod); +}; diff --git a/ultimmc/launcher/minecraft/OpSys.cpp b/ultimmc/launcher/minecraft/OpSys.cpp new file mode 100644 index 0000000..093ec41 --- /dev/null +++ b/ultimmc/launcher/minecraft/OpSys.cpp @@ -0,0 +1,46 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "OpSys.h" + +OpSys OpSys_fromString(QString name) +{ + if (name == "freebsd") + return Os_FreeBSD; + if (name == "linux") + return Os_Linux; + if (name == "windows") + return Os_Windows; + if (name == "osx") + return Os_OSX; + return Os_Other; +} + +QString OpSys_toString(OpSys name) +{ + switch (name) + { + case Os_FreeBSD: + return "freebsd"; + case Os_Linux: + return "linux"; + case Os_OSX: + return "osx"; + case Os_Windows: + return "windows"; + default: + return "other"; + } +} \ No newline at end of file diff --git a/ultimmc/launcher/minecraft/OpSys.h b/ultimmc/launcher/minecraft/OpSys.h new file mode 100644 index 0000000..0936f81 --- /dev/null +++ b/ultimmc/launcher/minecraft/OpSys.h @@ -0,0 +1,38 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +enum OpSys +{ + Os_Windows, + Os_FreeBSD, + Os_Linux, + Os_OSX, + Os_Other +}; + +OpSys OpSys_fromString(QString); +QString OpSys_toString(OpSys); + +#ifdef Q_OS_WIN32 + #define currentSystem Os_Windows +#elif defined Q_OS_MAC + #define currentSystem Os_OSX +#elif defined Q_OS_FREEBSD + #define currentSystem Os_FreeBSD +#else + #define currentSystem Os_Linux +#endif diff --git a/ultimmc/launcher/minecraft/PackProfile.cpp b/ultimmc/launcher/minecraft/PackProfile.cpp new file mode 100644 index 0000000..d69c79f --- /dev/null +++ b/ultimmc/launcher/minecraft/PackProfile.cpp @@ -0,0 +1,1226 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Exception.h" +#include "minecraft/OneSixVersionFormat.h" +#include "FileSystem.h" +#include "meta/Index.h" +#include "minecraft/MinecraftInstance.h" +#include "Json.h" + +#include "PackProfile.h" +#include "PackProfile_p.h" +#include "ComponentUpdateTask.h" + +#include "Application.h" + +PackProfile::PackProfile(MinecraftInstance * instance) + : QAbstractListModel() +{ + d.reset(new PackProfileData); + d->m_instance = instance; + d->m_saveTimer.setSingleShot(true); + d->m_saveTimer.setInterval(5000); + d->interactionDisabled = instance->isRunning(); + connect(d->m_instance, &BaseInstance::runningStatusChanged, this, &PackProfile::disableInteraction); + connect(&d->m_saveTimer, &QTimer::timeout, this, &PackProfile::save_internal); +} + +PackProfile::~PackProfile() +{ + saveNow(); +} + +// BEGIN: component file format + +static const int currentComponentsFileVersion = 1; + +static QJsonObject componentToJsonV1(ComponentPtr component) +{ + QJsonObject obj; + // critical + obj.insert("uid", component->m_uid); + if(!component->m_version.isEmpty()) + { + obj.insert("version", component->m_version); + } + if(component->m_dependencyOnly) + { + obj.insert("dependencyOnly", true); + } + if(component->m_important) + { + obj.insert("important", true); + } + if(component->m_disabled) + { + obj.insert("disabled", true); + } + + // cached + if(!component->m_cachedVersion.isEmpty()) + { + obj.insert("cachedVersion", component->m_cachedVersion); + } + if(!component->m_cachedName.isEmpty()) + { + obj.insert("cachedName", component->m_cachedName); + } + Meta::serializeRequires(obj, &component->m_cachedRequires, "cachedRequires"); + Meta::serializeRequires(obj, &component->m_cachedConflicts, "cachedConflicts"); + if(component->m_cachedVolatile) + { + obj.insert("cachedVolatile", true); + } + return obj; +} + +static ComponentPtr componentFromJsonV1(PackProfile * parent, const QString & componentJsonPattern, const QJsonObject &obj) +{ + // critical + auto uid = Json::requireValueString(obj.value("uid")); + auto filePath = componentJsonPattern.arg(uid); + auto component = new Component(parent, uid); + component->m_version = Json::ensureValueString(obj.value("version")); + component->m_dependencyOnly = Json::ensureValueBoolean(obj.value("dependencyOnly"), false); + component->m_important = Json::ensureValueBoolean(obj.value("important"), false); + + // cached + // TODO @RESILIENCE: ignore invalid values/structure here? + component->m_cachedVersion = Json::ensureValueString(obj.value("cachedVersion")); + component->m_cachedName = Json::ensureValueString(obj.value("cachedName")); + Meta::parseRequires(obj, &component->m_cachedRequires, "cachedRequires"); + Meta::parseRequires(obj, &component->m_cachedConflicts, "cachedConflicts"); + component->m_cachedVolatile = Json::ensureValueBoolean(obj.value("volatile"), false); + bool disabled = Json::ensureValueBoolean(obj.value("disabled"), false); + component->setEnabled(!disabled); + return component; +} + +// Save the given component container data to a file +static bool savePackProfile(const QString & filename, const ComponentContainer & container) +{ + QJsonObject obj; + obj.insert("formatVersion", currentComponentsFileVersion); + QJsonArray orderArray; + for(auto component: container) + { + orderArray.append(componentToJsonV1(component)); + } + obj.insert("components", orderArray); + QSaveFile outFile(filename); + if (!outFile.open(QFile::WriteOnly)) + { + qCritical() << "Couldn't open" << outFile.fileName() + << "for writing:" << outFile.errorString(); + return false; + } + auto data = QJsonDocument(obj).toJson(QJsonDocument::Indented); + if(outFile.write(data) != data.size()) + { + qCritical() << "Couldn't write all the data into" << outFile.fileName() + << "because:" << outFile.errorString(); + return false; + } + if(!outFile.commit()) + { + qCritical() << "Couldn't save" << outFile.fileName() + << "because:" << outFile.errorString(); + } + return true; +} + +// Read the given file into component containers +static bool loadPackProfile(PackProfile * parent, const QString & filename, const QString & componentJsonPattern, ComponentContainer & container) +{ + QFile componentsFile(filename); + if (!componentsFile.exists()) + { + qWarning() << "Components file doesn't exist. This should never happen."; + return false; + } + if (!componentsFile.open(QFile::ReadOnly)) + { + qCritical() << "Couldn't open" << componentsFile.fileName() + << " for reading:" << componentsFile.errorString(); + qWarning() << "Ignoring overriden order"; + return false; + } + + // and it's valid JSON + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(componentsFile.readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + qCritical() << "Couldn't parse" << componentsFile.fileName() << ":" << error.errorString(); + qWarning() << "Ignoring overriden order"; + return false; + } + + // and then read it and process it if all above is true. + try + { + auto obj = Json::requireObject(doc); + // check order file version. + auto version = Json::requireValueInteger(obj.value("formatVersion")); + if (version != currentComponentsFileVersion) + { + throw JSONValidationError(QObject::tr("Invalid component file version, expected %1") + .arg(currentComponentsFileVersion)); + } + auto orderArray = Json::requireValueArray(obj.value("components")); + for(auto item: orderArray) + { + auto obj = Json::requireValueObject(item, "Component must be an object."); + container.append(componentFromJsonV1(parent, componentJsonPattern, obj)); + } + } + catch (const JSONValidationError &err) + { + qCritical() << "Couldn't parse" << componentsFile.fileName() << ": bad file format"; + container.clear(); + return false; + } + return true; +} + +// END: component file format + +// BEGIN: save/load logic + +void PackProfile::saveNow() +{ + if(saveIsScheduled()) + { + d->m_saveTimer.stop(); + save_internal(); + } +} + +bool PackProfile::saveIsScheduled() const +{ + return d->dirty; +} + +void PackProfile::buildingFromScratch() +{ + d->loaded = true; + d->dirty = true; +} + +void PackProfile::scheduleSave() +{ + if(!d->loaded) + { + qDebug() << "Component list should never save if it didn't successfully load, instance:" << d->m_instance->name(); + return; + } + if(!d->dirty) + { + d->dirty = true; + qDebug() << "Component list save is scheduled for" << d->m_instance->name(); + } + d->m_saveTimer.start(); +} + +QString PackProfile::componentsFilePath() const +{ + return FS::PathCombine(d->m_instance->instanceRoot(), "mmc-pack.json"); +} + +QString PackProfile::patchesPattern() const +{ + return FS::PathCombine(d->m_instance->instanceRoot(), "patches", "%1.json"); +} + +QString PackProfile::patchFilePathForUid(const QString& uid) const +{ + return patchesPattern().arg(uid); +} + +void PackProfile::save_internal() +{ + qDebug() << "Component list save performed now for" << d->m_instance->name(); + auto filename = componentsFilePath(); + savePackProfile(filename, d->components); + d->dirty = false; +} + +bool PackProfile::load() +{ + auto filename = componentsFilePath(); + QFile componentsFile(filename); + + // migrate old config to new one, if needed + if(!componentsFile.exists()) + { + if(!migratePreComponentConfig()) + { + // FIXME: the user should be notified... + qCritical() << "Failed to convert old pre-component config for instance" << d->m_instance->name(); + return false; + } + } + + // load the new component list and swap it with the current one... + ComponentContainer newComponents; + if(!loadPackProfile(this, filename, patchesPattern(), newComponents)) + { + qCritical() << "Failed to load the component config for instance" << d->m_instance->name(); + return false; + } + else + { + // FIXME: actually use fine-grained updates, not this... + beginResetModel(); + // disconnect all the old components + for(auto component: d->components) + { + disconnect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); + } + d->components.clear(); + d->componentIndex.clear(); + for(auto component: newComponents) + { + if(d->componentIndex.contains(component->m_uid)) + { + qWarning() << "Ignoring duplicate component entry" << component->m_uid; + continue; + } + connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); + d->components.append(component); + d->componentIndex[component->m_uid] = component; + } + endResetModel(); + d->loaded = true; + return true; + } +} + +void PackProfile::reload(Net::Mode netmode) +{ + // Do not reload when the update/resolve task is running. It is in control. + if(d->m_updateTask) + { + return; + } + + // flush any scheduled saves to not lose state + saveNow(); + + // FIXME: differentiate when a reapply is required by propagating state from components + invalidateLaunchProfile(); + + if(load()) + { + resolve(netmode); + } +} + +Task::Ptr PackProfile::getCurrentTask() +{ + return d->m_updateTask; +} + +void PackProfile::resolve(Net::Mode netmode) +{ + auto updateTask = new ComponentUpdateTask(ComponentUpdateTask::Mode::Resolution, netmode, this); + d->m_updateTask.reset(updateTask); + connect(updateTask, &ComponentUpdateTask::succeeded, this, &PackProfile::updateSucceeded); + connect(updateTask, &ComponentUpdateTask::failed, this, &PackProfile::updateFailed); + d->m_updateTask->start(); +} + + +void PackProfile::updateSucceeded() +{ + qDebug() << "Component list update/resolve task succeeded for" << d->m_instance->name(); + d->m_updateTask.reset(); + invalidateLaunchProfile(); +} + +void PackProfile::updateFailed(const QString& error) +{ + qDebug() << "Component list update/resolve task failed for" << d->m_instance->name() << "Reason:" << error; + d->m_updateTask.reset(); + invalidateLaunchProfile(); +} + +// NOTE this is really old stuff, and only needs to be used when loading the old hardcoded component-unaware format (loadPreComponentConfig). +static void upgradeDeprecatedFiles(QString root, QString instanceName) +{ + auto versionJsonPath = FS::PathCombine(root, "version.json"); + auto customJsonPath = FS::PathCombine(root, "custom.json"); + auto mcJson = FS::PathCombine(root, "patches" , "net.minecraft.json"); + + QString sourceFile; + QString renameFile; + + // convert old crap. + if(QFile::exists(customJsonPath)) + { + sourceFile = customJsonPath; + renameFile = versionJsonPath; + } + else if(QFile::exists(versionJsonPath)) + { + sourceFile = versionJsonPath; + } + if(!sourceFile.isEmpty() && !QFile::exists(mcJson)) + { + if(!FS::ensureFilePathExists(mcJson)) + { + qWarning() << "Couldn't create patches folder for" << instanceName; + return; + } + if(!renameFile.isEmpty() && QFile::exists(renameFile)) + { + if(!QFile::rename(renameFile, renameFile + ".old")) + { + qWarning() << "Couldn't rename" << renameFile << "to" << renameFile + ".old" << "in" << instanceName; + return; + } + } + auto file = ProfileUtils::parseJsonFile(QFileInfo(sourceFile), false); + ProfileUtils::removeLwjglFromPatch(file); + file->uid = "net.minecraft"; + file->version = file->minecraftVersion; + file->name = "Minecraft"; + + Meta::Require needsLwjgl; + needsLwjgl.uid = "org.lwjgl"; + file->depends.insert(needsLwjgl); + + if(!ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), mcJson)) + { + return; + } + if(!QFile::rename(sourceFile, sourceFile + ".old")) + { + qWarning() << "Couldn't rename" << sourceFile << "to" << sourceFile + ".old" << "in" << instanceName; + return; + } + } +} + +/* + * Migrate old layout to the component based one... + * - Part of the version information is taken from `instance.cfg` (fed to this class from outside). + * - Part is taken from the old order.json file. + * - Part is loaded from loose json files in the instance's `patches` directory. + */ +bool PackProfile::migratePreComponentConfig() +{ + // upgrade the very old files from the beginnings of MultiMC 5 + upgradeDeprecatedFiles(d->m_instance->instanceRoot(), d->m_instance->name()); + + QList components; + QSet loaded; + + auto addBuiltinPatch = [&](const QString &uid, bool asDependency, const QString & emptyVersion, const Meta::Require & req, const Meta::Require & conflict) + { + auto jsonFilePath = FS::PathCombine(d->m_instance->instanceRoot(), "patches" , uid + ".json"); + auto intendedVersion = d->getOldConfigVersion(uid); + // load up the base minecraft patch + ComponentPtr component; + if(QFile::exists(jsonFilePath)) + { + if(intendedVersion.isEmpty()) + { + intendedVersion = emptyVersion; + } + auto file = ProfileUtils::parseJsonFile(QFileInfo(jsonFilePath), false); + // fix uid + file->uid = uid; + // if version is missing, add it from the outside. + if(file->version.isEmpty()) + { + file->version = intendedVersion; + } + // if this is a dependency (LWJGL), mark it also as volatile + if(asDependency) + { + file->m_volatile = true; + } + // insert requirements if needed + if(!req.uid.isEmpty()) + { + file->depends.insert(req); + } + // insert conflicts if needed + if(!conflict.uid.isEmpty()) + { + file->conflicts.insert(conflict); + } + // FIXME: @QUALITY do not ignore return value + ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), jsonFilePath); + component = new Component(this, uid, file); + component->m_version = intendedVersion; + } + else if(!intendedVersion.isEmpty()) + { + auto metaVersion = APPLICATION->metadataIndex()->get(uid, intendedVersion); + component = new Component(this, metaVersion); + } + else + { + return; + } + component->m_dependencyOnly = asDependency; + component->m_important = !asDependency; + components.append(component); + }; + // TODO: insert depends and conflicts here if these are customized files... + Meta::Require reqLwjgl; + reqLwjgl.uid = "org.lwjgl"; + reqLwjgl.suggests = "2.9.1"; + Meta::Require conflictLwjgl3; + conflictLwjgl3.uid = "org.lwjgl3"; + Meta::Require nullReq; + addBuiltinPatch("org.lwjgl", true, "2.9.1", nullReq, conflictLwjgl3); + addBuiltinPatch("net.minecraft", false, QString(), reqLwjgl, nullReq); + + // first, collect all other file-based patches and load them + QMap loadedComponents; + QDir patchesDir(FS::PathCombine(d->m_instance->instanceRoot(),"patches")); + for (auto info : patchesDir.entryInfoList(QStringList() << "*.json", QDir::Files)) + { + // parse the file + qDebug() << "Reading" << info.fileName(); + auto file = ProfileUtils::parseJsonFile(info, true); + + // correct missing or wrong uid based on the file name + QString uid = info.completeBaseName(); + + // ignore builtins, they've been handled already + if (uid == "net.minecraft") + continue; + if (uid == "org.lwjgl") + continue; + + // handle horrible corner cases + if(uid.isEmpty()) + { + // if you have a file named '.json', make it just go away. + // FIXME: @QUALITY do not ignore return value + QFile::remove(info.absoluteFilePath()); + continue; + } + file->uid = uid; + // FIXME: @QUALITY do not ignore return value + ProfileUtils::saveJsonFile(OneSixVersionFormat::versionFileToJson(file), info.absoluteFilePath()); + + auto component = new Component(this, file->uid, file); + auto version = d->getOldConfigVersion(file->uid); + if(!version.isEmpty()) + { + component->m_version = version; + } + loadedComponents[file->uid] = component; + } + // try to load the other 'hardcoded' patches (forge, liteloader), if they weren't loaded from files + auto loadSpecial = [&](const QString & uid, int order) + { + auto patchVersion = d->getOldConfigVersion(uid); + if(!patchVersion.isEmpty() && !loadedComponents.contains(uid)) + { + auto patch = new Component(this, APPLICATION->metadataIndex()->get(uid, patchVersion)); + patch->setOrder(order); + loadedComponents[uid] = patch; + } + }; + loadSpecial("net.minecraftforge", 5); + loadSpecial("com.mumfrey.liteloader", 10); + + // load the old order.json file, if present + ProfileUtils::PatchOrder userOrder; + ProfileUtils::readOverrideOrders(FS::PathCombine(d->m_instance->instanceRoot(), "order.json"), userOrder); + + // now add all the patches by user sort order + for (auto uid : userOrder) + { + // ignore builtins + if (uid == "net.minecraft") + continue; + if (uid == "org.lwjgl") + continue; + // ordering has a patch that is gone? + if(!loadedComponents.contains(uid)) + { + continue; + } + components.append(loadedComponents.take(uid)); + } + + // is there anything left to sort? - this is used when there are leftover components that aren't part of the order.json + if(!loadedComponents.isEmpty()) + { + // inserting into multimap by order number as key sorts the patches and detects duplicates + QMultiMap files; + auto iter = loadedComponents.begin(); + while(iter != loadedComponents.end()) + { + files.insert((*iter)->getOrder(), *iter); + iter++; + } + + // then just extract the patches and put them in the list + for (auto order : files.keys()) + { + const auto &values = files.values(order); + for(auto &value: values) + { + // TODO: put back the insertion of problem messages here, so the user knows about the id duplication + components.append(value); + } + } + } + // new we have a complete list of components... + return savePackProfile(componentsFilePath(), components); +} + +// END: save/load + +void PackProfile::appendComponent(ComponentPtr component) +{ + insertComponent(d->components.size(), component); +} + +void PackProfile::insertComponent(size_t index, ComponentPtr component) +{ + auto id = component->getID(); + if(id.isEmpty()) + { + qWarning() << "Attempt to add a component with empty ID!"; + return; + } + if(d->componentIndex.contains(id)) + { + qWarning() << "Attempt to add a component that is already present!"; + return; + } + beginInsertRows(QModelIndex(), index, index); + d->components.insert(index, component); + d->componentIndex[id] = component; + endInsertRows(); + connect(component.get(), &Component::dataChanged, this, &PackProfile::componentDataChanged); + scheduleSave(); +} + +void PackProfile::componentDataChanged() +{ + auto objPtr = qobject_cast(sender()); + if(!objPtr) + { + qWarning() << "PackProfile got dataChenged signal from a non-Component!"; + return; + } + if(objPtr->getID() == "net.minecraft") { + emit minecraftChanged(); + } + // figure out which one is it... in a seriously dumb way. + int index = 0; + for (auto component: d->components) + { + if(component.get() == objPtr) + { + emit dataChanged(createIndex(index, 0), createIndex(index, columnCount(QModelIndex()) - 1)); + scheduleSave(); + return; + } + index++; + } + qWarning() << "PackProfile got dataChenged signal from a Component which does not belong to it!"; +} + +bool PackProfile::remove(const int index) +{ + auto patch = getComponent(index); + if (!patch->isRemovable()) + { + qWarning() << "Patch" << patch->getID() << "is non-removable"; + return false; + } + + if(!removeComponent_internal(patch)) + { + qCritical() << "Patch" << patch->getID() << "could not be removed"; + return false; + } + + beginRemoveRows(QModelIndex(), index, index); + d->components.removeAt(index); + d->componentIndex.remove(patch->getID()); + endRemoveRows(); + invalidateLaunchProfile(); + scheduleSave(); + return true; +} + +bool PackProfile::remove(const QString id) +{ + int i = 0; + for (auto patch : d->components) + { + if (patch->getID() == id) + { + return remove(i); + } + i++; + } + return false; +} + +bool PackProfile::customize(int index) +{ + auto patch = getComponent(index); + if (!patch->isCustomizable()) + { + qDebug() << "Patch" << patch->getID() << "is not customizable"; + return false; + } + if(!patch->customize()) + { + qCritical() << "Patch" << patch->getID() << "could not be customized"; + return false; + } + invalidateLaunchProfile(); + scheduleSave(); + return true; +} + +bool PackProfile::revertToBase(int index) +{ + auto patch = getComponent(index); + if (!patch->isRevertible()) + { + qDebug() << "Patch" << patch->getID() << "is not revertible"; + return false; + } + if(!patch->revert()) + { + qCritical() << "Patch" << patch->getID() << "could not be reverted"; + return false; + } + invalidateLaunchProfile(); + scheduleSave(); + return true; +} + +Component * PackProfile::getComponent(const QString &id) +{ + auto iter = d->componentIndex.find(id); + if (iter == d->componentIndex.end()) + { + return nullptr; + } + return (*iter).get(); +} + +Component * PackProfile::getComponent(int index) +{ + if(index < 0 || index >= d->components.size()) + { + return nullptr; + } + return d->components[index].get(); +} + +QVariant PackProfile::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= d->components.size()) + return QVariant(); + + auto patch = d->components.at(row); + + switch (role) + { + case Qt::CheckStateRole: + { + switch (column) + { + case NameColumn: { + return patch->isEnabled() ? Qt::Checked : Qt::Unchecked; + } + default: + return QVariant(); + } + } + case Qt::DisplayRole: + { + switch (column) + { + case NameColumn: + return patch->getName(); + case VersionColumn: + { + if(patch->isCustom()) + { + return QString("%1 (Custom)").arg(patch->getVersion()); + } + else + { + return patch->getVersion(); + } + } + default: + return QVariant(); + } + } + case Qt::DecorationRole: + { + switch(column) + { + case NameColumn: + { + auto severity = patch->getProblemSeverity(); + switch (severity) + { + case ProblemSeverity::Warning: + return "warning"; + case ProblemSeverity::Error: + return "error"; + default: + return QVariant(); + } + } + default: + { + return QVariant(); + } + } + } + } + return QVariant(); +} + +bool PackProfile::setData(const QModelIndex& index, const QVariant& value, int role) +{ + if (!index.isValid() || index.row() < 0 || index.row() >= rowCount(index)) + { + return false; + } + + if (role == Qt::CheckStateRole) + { + auto component = d->components[index.row()]; + if (component->setEnabled(!component->isEnabled())) + { + return true; + } + } + return false; +} + +QVariant PackProfile::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Horizontal) + { + if (role == Qt::DisplayRole) + { + switch (section) + { + case NameColumn: + return tr("Name"); + case VersionColumn: + return tr("Version"); + default: + return QVariant(); + } + } + } + return QVariant(); +} + +// FIXME: zero precision mess +Qt::ItemFlags PackProfile::flags(const QModelIndex &index) const +{ + if (!index.isValid()) { + return Qt::NoItemFlags; + } + + Qt::ItemFlags outFlags = Qt::ItemIsSelectable | Qt::ItemIsEnabled; + + int row = index.row(); + + if (row < 0 || row >= d->components.size()) { + return Qt::NoItemFlags; + } + + auto patch = d->components.at(row); + // TODO: this will need fine-tuning later... + if(patch->canBeDisabled() && !d->interactionDisabled) + { + outFlags |= Qt::ItemIsUserCheckable; + } + return outFlags; +} + +int PackProfile::rowCount(const QModelIndex &parent) const +{ + return d->components.size(); +} + +int PackProfile::columnCount(const QModelIndex &parent) const +{ + return NUM_COLUMNS; +} + +void PackProfile::move(const int index, const MoveDirection direction) +{ + int theirIndex; + if (direction == MoveUp) + { + theirIndex = index - 1; + } + else + { + theirIndex = index + 1; + } + + if (index < 0 || index >= d->components.size()) + return; + if (theirIndex >= rowCount()) + theirIndex = rowCount() - 1; + if (theirIndex == -1) + theirIndex = rowCount() - 1; + if (index == theirIndex) + return; + int togap = theirIndex > index ? theirIndex + 1 : theirIndex; + + auto from = getComponent(index); + auto to = getComponent(theirIndex); + + if (!from || !to || !to->isMoveable() || !from->isMoveable()) + { + return; + } + beginMoveRows(QModelIndex(), index, index, QModelIndex(), togap); + d->components.swap(index, theirIndex); + endMoveRows(); + invalidateLaunchProfile(); + scheduleSave(); +} + +void PackProfile::invalidateLaunchProfile() +{ + d->m_profile.reset(); +} + +void PackProfile::installJarMods(QStringList selectedFiles) +{ + installJarMods_internal(selectedFiles); +} + +void PackProfile::installCustomJar(QString selectedFile) +{ + installCustomJar_internal(selectedFile); +} + +bool PackProfile::installEmpty(const QString& uid, const QString& name) +{ + QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if(!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + auto f = std::make_shared(); + f->name = name; + f->uid = uid; + f->version = "1"; + QString patchFileName = FS::PathCombine(patchDir, uid + ".json"); + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + appendComponent(new Component(this, f->uid, f)); + scheduleSave(); + invalidateLaunchProfile(); + return true; +} + +bool PackProfile::removeComponent_internal(ComponentPtr patch) +{ + bool ok = true; + // first, remove the patch file. this ensures it's not used anymore + auto fileName = patch->getFilename(); + if(fileName.size()) + { + QFile patchFile(fileName); + if(patchFile.exists() && !patchFile.remove()) + { + qCritical() << "File" << fileName << "could not be removed because:" << patchFile.errorString(); + return false; + } + } + + // FIXME: we need a generic way of removing local resources, not just jar mods... + auto preRemoveJarMod = [&](LibraryPtr jarMod) -> bool + { + if (!jarMod->isLocal()) + { + return true; + } + QStringList jar, temp1, temp2, temp3; + jarMod->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, d->m_instance->jarmodsPath().absolutePath()); + QFileInfo finfo (jar[0]); + if(finfo.exists()) + { + QFile jarModFile(jar[0]); + if(!jarModFile.remove()) + { + qCritical() << "File" << jar[0] << "could not be removed because:" << jarModFile.errorString(); + return false; + } + return true; + } + return true; + }; + + auto vFile = patch->getVersionFile(); + if(vFile) + { + auto &jarMods = vFile->jarMods; + for(auto &jarmod: jarMods) + { + ok &= preRemoveJarMod(jarmod); + } + } + return ok; +} + +bool PackProfile::installJarMods_internal(QStringList filepaths) +{ + QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if(!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + + if (!FS::ensureFolderPathExists(d->m_instance->jarModsDir())) + { + return false; + } + + for(auto filepath:filepaths) + { + QFileInfo sourceInfo(filepath); + auto uuid = QUuid::createUuid(); + QString id = uuid.toString().remove('{').remove('}'); + QString target_filename = id + ".jar"; + QString target_id = "org.multimc.jarmod." + id; + QString target_name = sourceInfo.completeBaseName() + " (jar mod)"; + QString finalPath = FS::PathCombine(d->m_instance->jarModsDir(), target_filename); + + QFileInfo targetInfo(finalPath); + if(targetInfo.exists()) + { + return false; + } + + if (!QFile::copy(sourceInfo.absoluteFilePath(),QFileInfo(finalPath).absoluteFilePath())) + { + return false; + } + + auto f = std::make_shared(); + auto jarMod = std::make_shared(); + jarMod->setRawName(GradleSpecifier("org.multimc.jarmods:" + id + ":1")); + jarMod->setFilename(target_filename); + jarMod->setDisplayName(sourceInfo.completeBaseName()); + jarMod->setHint("local"); + f->jarMods.append(jarMod); + f->name = target_name; + f->uid = target_id; + QString patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + appendComponent(new Component(this, f->uid, f)); + } + scheduleSave(); + invalidateLaunchProfile(); + return true; +} + +bool PackProfile::installCustomJar_internal(QString filepath) +{ + QString patchDir = FS::PathCombine(d->m_instance->instanceRoot(), "patches"); + if(!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + + QString libDir = d->m_instance->getLocalLibraryPath(); + if (!FS::ensureFolderPathExists(libDir)) + { + return false; + } + + auto specifier = GradleSpecifier("org.multimc:customjar:1"); + QFileInfo sourceInfo(filepath); + QString target_filename = specifier.getFileName(); + QString target_id = specifier.artifactId(); + QString target_name = sourceInfo.completeBaseName() + " (custom jar)"; + QString finalPath = FS::PathCombine(libDir, target_filename); + + QFileInfo jarInfo(finalPath); + if (jarInfo.exists()) + { + if(!QFile::remove(finalPath)) + { + return false; + } + } + if (!QFile::copy(filepath, finalPath)) + { + return false; + } + + auto f = std::make_shared(); + auto jarMod = std::make_shared(); + jarMod->setRawName(specifier); + jarMod->setDisplayName(sourceInfo.completeBaseName()); + jarMod->setHint("local"); + f->mainJar = jarMod; + f->name = target_name; + f->uid = target_id; + QString patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + appendComponent(new Component(this, f->uid, f)); + + scheduleSave(); + invalidateLaunchProfile(); + return true; +} + +std::shared_ptr PackProfile::getProfile() const +{ + if(!d->m_profile) + { + try + { + auto profile = std::make_shared(); + for(auto file: d->components) + { + qDebug() << "Applying" << file->getID() << (file->getProblemSeverity() == ProblemSeverity::Error ? "ERROR" : "GOOD"); + file->applyTo(profile.get()); + } + d->m_profile = profile; + } + catch (const Exception &error) + { + qWarning() << "Couldn't apply profile patches because: " << error.cause(); + } + } + return d->m_profile; +} + +void PackProfile::setOldConfigVersion(const QString& uid, const QString& version) +{ + if(version.isEmpty()) + { + return; + } + d->m_oldConfigVersions[uid] = version; +} + +bool PackProfile::setComponentVersion(const QString& uid, const QString& version, bool important) +{ + auto iter = d->componentIndex.find(uid); + if(iter != d->componentIndex.end()) + { + ComponentPtr component = *iter; + // set existing + if(component->revert()) + { + component->setVersion(version); + component->setImportant(important); + return true; + } + return false; + } + else + { + // add new + auto component = new Component(this, uid); + component->m_version = version; + component->m_important = important; + appendComponent(component); + return true; + } +} + +QString PackProfile::getComponentVersion(const QString& uid) const +{ + const auto iter = d->componentIndex.find(uid); + if (iter != d->componentIndex.end()) + { + return (*iter)->getVersion(); + } + return QString(); +} + +void PackProfile::disableInteraction(bool disable) +{ + if(d->interactionDisabled != disable) { + d->interactionDisabled = disable; + auto size = d->components.size(); + if(size) { + emit dataChanged(index(0), index(size - 1)); + } + } +} diff --git a/ultimmc/launcher/minecraft/PackProfile.h b/ultimmc/launcher/minecraft/PackProfile.h new file mode 100644 index 0000000..f30deb5 --- /dev/null +++ b/ultimmc/launcher/minecraft/PackProfile.h @@ -0,0 +1,151 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include + +#include "Library.h" +#include "LaunchProfile.h" +#include "Component.h" +#include "ProfileUtils.h" +#include "BaseVersion.h" +#include "MojangDownloadInfo.h" +#include "net/Mode.h" + +class MinecraftInstance; +struct PackProfileData; +class ComponentUpdateTask; + +class PackProfile : public QAbstractListModel +{ + Q_OBJECT + friend ComponentUpdateTask; +public: + enum Columns + { + NameColumn = 0, + VersionColumn, + NUM_COLUMNS + }; + + explicit PackProfile(MinecraftInstance * instance); + virtual ~PackProfile(); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override; + virtual int columnCount(const QModelIndex &parent) const override; + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + + /// call this to explicitly mark the component list as loaded - this is used to build a new component list from scratch. + void buildingFromScratch(); + + /// install more jar mods + void installJarMods(QStringList selectedFiles); + + /// install a jar/zip as a replacement for the main jar + void installCustomJar(QString selectedFile); + + enum MoveDirection { MoveUp, MoveDown }; + /// move component file # up or down the list + void move(const int index, const MoveDirection direction); + + /// remove component file # - including files/records + bool remove(const int index); + + /// remove component file by id - including files/records + bool remove(const QString id); + + bool customize(int index); + + bool revertToBase(int index); + + /// reload the list, reload all components, resolve dependencies + void reload(Net::Mode netmode); + + // reload all components, resolve dependencies + void resolve(Net::Mode netmode); + + /// get current running task... + Task::Ptr getCurrentTask(); + + std::shared_ptr getProfile() const; + + // NOTE: used ONLY by MinecraftInstance to provide legacy version mappings from instance config + void setOldConfigVersion(const QString &uid, const QString &version); + + QString getComponentVersion(const QString &uid) const; + + bool setComponentVersion(const QString &uid, const QString &version, bool important = false); + + bool installEmpty(const QString &uid, const QString &name); + + QString patchFilePathForUid(const QString &uid) const; + + /// if there is a save scheduled, do it now. + void saveNow(); + +signals: + void minecraftChanged(); + +public: + /// get the profile component by id + Component * getComponent(const QString &id); + + /// get the profile component by index + Component * getComponent(int index); + + /// Add the component to the internal list of patches + // todo(merged): is this the best approach + void appendComponent(ComponentPtr component); + +private: + void scheduleSave(); + bool saveIsScheduled() const; + + /// apply the component patches. Catches all the errors and returns true/false for success/failure + void invalidateLaunchProfile(); + + /// insert component so that its index is ideally the specified one (returns real index) + void insertComponent(size_t index, ComponentPtr component); + + QString componentsFilePath() const; + QString patchesPattern() const; + +private slots: + void save_internal(); + void updateSucceeded(); + void updateFailed(const QString & error); + void componentDataChanged(); + void disableInteraction(bool disable); + +private: + bool load(); + bool installJarMods_internal(QStringList filepaths); + bool installCustomJar_internal(QString filepath); + bool removeComponent_internal(ComponentPtr patch); + + bool migratePreComponentConfig(); + +private: /* data */ + + std::unique_ptr d; +}; diff --git a/ultimmc/launcher/minecraft/PackProfile_p.h b/ultimmc/launcher/minecraft/PackProfile_p.h new file mode 100644 index 0000000..fce921b --- /dev/null +++ b/ultimmc/launcher/minecraft/PackProfile_p.h @@ -0,0 +1,42 @@ +#pragma once + +#include "Component.h" +#include +#include +#include +#include + +class MinecraftInstance; +using ComponentContainer = QList; +using ComponentIndex = QMap; + +struct PackProfileData +{ + // the instance this belongs to + MinecraftInstance *m_instance; + + // the launch profile (volatile, temporary thing created on demand) + std::shared_ptr m_profile; + + // version information migrated from instance.cfg file. Single use on migration! + std::map m_oldConfigVersions; + QString getOldConfigVersion(const QString& uid) const + { + const auto iter = m_oldConfigVersions.find(uid); + if(iter != m_oldConfigVersions.cend()) + { + return (*iter).second; + } + return QString(); + } + + // persistent list of components and related machinery + ComponentContainer components; + ComponentIndex componentIndex; + bool dirty = false; + QTimer m_saveTimer; + Task::Ptr m_updateTask; + bool loaded = false; + bool interactionDisabled = true; +}; + diff --git a/ultimmc/launcher/minecraft/ParseUtils.cpp b/ultimmc/launcher/minecraft/ParseUtils.cpp new file mode 100644 index 0000000..c9640e7 --- /dev/null +++ b/ultimmc/launcher/minecraft/ParseUtils.cpp @@ -0,0 +1,34 @@ +#include +#include +#include "ParseUtils.h" +#include +#include + +QDateTime timeFromS3Time(QString str) +{ + return QDateTime::fromString(str, Qt::ISODate); +} + +QString timeToS3Time(QDateTime time) +{ + // this all because Qt can't format timestamps right. + int offsetRaw = time.offsetFromUtc(); + bool negative = offsetRaw < 0; + int offsetAbs = std::abs(offsetRaw); + + int offsetSeconds = offsetAbs % 60; + offsetAbs -= offsetSeconds; + + int offsetMinutes = offsetAbs % 3600; + offsetAbs -= offsetMinutes; + offsetMinutes /= 60; + + int offsetHours = offsetAbs / 3600; + + QString raw = time.toString("yyyy-MM-ddTHH:mm:ss"); + raw += (negative ? QChar('-') : QChar('+')); + raw += QString("%1").arg(offsetHours, 2, 10, QChar('0')); + raw += ":"; + raw += QString("%1").arg(offsetMinutes, 2, 10, QChar('0')); + return raw; +} diff --git a/ultimmc/launcher/minecraft/ParseUtils.h b/ultimmc/launcher/minecraft/ParseUtils.h new file mode 100644 index 0000000..aad8274 --- /dev/null +++ b/ultimmc/launcher/minecraft/ParseUtils.h @@ -0,0 +1,9 @@ +#pragma once +#include +#include + +/// take the timestamp used by S3 and turn it into QDateTime +QDateTime timeFromS3Time(QString str); + +/// take a timestamp and convert it into an S3 timestamp +QString timeToS3Time(QDateTime); diff --git a/ultimmc/launcher/minecraft/ParseUtils_test.cpp b/ultimmc/launcher/minecraft/ParseUtils_test.cpp new file mode 100644 index 0000000..fcc137e --- /dev/null +++ b/ultimmc/launcher/minecraft/ParseUtils_test.cpp @@ -0,0 +1,45 @@ +#include +#include "TestUtil.h" + +#include "minecraft/ParseUtils.h" + +class ParseUtilsTest : public QObject +{ + Q_OBJECT +private +slots: + void test_Through_data() + { + QTest::addColumn("timestamp"); + const char * timestamps[] = + { + "2016-02-29T13:49:54+01:00", + "2016-02-26T15:21:11+00:01", + "2016-02-24T15:52:36+01:13", + "2016-02-18T17:41:00+00:00", + "2016-02-17T15:23:19+00:00", + "2016-02-16T15:22:39+09:22", + "2016-02-10T15:06:41+00:00", + "2016-02-04T15:28:02-05:33" + }; + for(unsigned i = 0; i < (sizeof(timestamps) / sizeof(const char *)); i++) + { + QTest::newRow(timestamps[i]) << QString(timestamps[i]); + } + } + void test_Through() + { + QFETCH(QString, timestamp); + + auto time_parsed = timeFromS3Time(timestamp); + auto time_serialized = timeToS3Time(time_parsed); + + QCOMPARE(time_serialized, timestamp); + } + +}; + +QTEST_GUILESS_MAIN(ParseUtilsTest) + +#include "ParseUtils_test.moc" + diff --git a/ultimmc/launcher/minecraft/ProfileUtils.cpp b/ultimmc/launcher/minecraft/ProfileUtils.cpp new file mode 100644 index 0000000..b8b905c --- /dev/null +++ b/ultimmc/launcher/minecraft/ProfileUtils.cpp @@ -0,0 +1,178 @@ +#include "ProfileUtils.h" +#include "minecraft/VersionFilterData.h" +#include "minecraft/OneSixVersionFormat.h" +#include "Json.h" +#include + +#include +#include +#include +#include + +namespace ProfileUtils +{ + +static const int currentOrderFileVersion = 1; + +bool readOverrideOrders(QString path, PatchOrder &order) +{ + QFile orderFile(path); + if (!orderFile.exists()) + { + qWarning() << "Order file doesn't exist. Ignoring."; + return false; + } + if (!orderFile.open(QFile::ReadOnly)) + { + qCritical() << "Couldn't open" << orderFile.fileName() + << " for reading:" << orderFile.errorString(); + qWarning() << "Ignoring overriden order"; + return false; + } + + // and it's valid JSON + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(orderFile.readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + qCritical() << "Couldn't parse" << orderFile.fileName() << ":" << error.errorString(); + qWarning() << "Ignoring overriden order"; + return false; + } + + // and then read it and process it if all above is true. + try + { + auto obj = Json::requireObject(doc); + // check order file version. + auto version = Json::requireValueInteger(obj.value("version")); + if (version != currentOrderFileVersion) + { + throw JSONValidationError(QObject::tr("Invalid order file version, expected %1") + .arg(currentOrderFileVersion)); + } + auto orderArray = Json::requireValueArray(obj.value("order")); + for(auto item: orderArray) + { + order.append(Json::requireValueString(item)); + } + } + catch (const JSONValidationError &err) + { + qCritical() << "Couldn't parse" << orderFile.fileName() << ": bad file format"; + qWarning() << "Ignoring overriden order"; + order.clear(); + return false; + } + return true; +} + +static VersionFilePtr createErrorVersionFile(QString fileId, QString filepath, QString error) +{ + auto outError = std::make_shared(); + outError->uid = outError->name = fileId; + // outError->filename = filepath; + outError->addProblem(ProblemSeverity::Error, error); + return outError; +} + +static VersionFilePtr guardedParseJson(const QJsonDocument & doc,const QString &fileId,const QString &filepath,const bool &requireOrder) +{ + try + { + return OneSixVersionFormat::versionFileFromJson(doc, filepath, requireOrder); + } + catch (const Exception &e) + { + return createErrorVersionFile(fileId, filepath, e.cause()); + } +} + +VersionFilePtr parseJsonFile(const QFileInfo &fileInfo, const bool requireOrder) +{ + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QFile::ReadOnly)) + { + auto errorStr = QObject::tr("Unable to open the version file %1: %2.").arg(fileInfo.fileName(), file.errorString()); + return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); + } + QJsonParseError error; + auto data = file.readAll(); + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + file.close(); + if (error.error != QJsonParseError::NoError) + { + int line = 1; + int column = 0; + for(int i = 0; i < error.offset; i++) + { + if(data[i] == '\n') + { + line++; + column = 0; + continue; + } + column++; + } + auto errorStr = QObject::tr("Unable to process the version file %1: %2 at line %3 column %4.") + .arg(fileInfo.fileName(), error.errorString()) + .arg(line).arg(column); + return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); + } + return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), requireOrder); +} + +bool saveJsonFile(const QJsonDocument doc, const QString & filename) +{ + auto data = doc.toJson(); + QSaveFile jsonFile(filename); + if(!jsonFile.open(QIODevice::WriteOnly)) + { + jsonFile.cancelWriting(); + qWarning() << "Couldn't open" << filename << "for writing"; + return false; + } + jsonFile.write(data); + if(!jsonFile.commit()) + { + qWarning() << "Couldn't save" << filename; + return false; + } + return true; +} + +VersionFilePtr parseBinaryJsonFile(const QFileInfo &fileInfo) +{ + QFile file(fileInfo.absoluteFilePath()); + if (!file.open(QFile::ReadOnly)) + { + auto errorStr = QObject::tr("Unable to open the version file %1: %2.").arg(fileInfo.fileName(), file.errorString()); + return createErrorVersionFile(fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), errorStr); + } + QJsonDocument doc = QJsonDocument::fromBinaryData(file.readAll()); + file.close(); + if (doc.isNull()) + { + file.remove(); + throw JSONValidationError(QObject::tr("Unable to process the version file %1.").arg(fileInfo.fileName())); + } + return guardedParseJson(doc, fileInfo.completeBaseName(), fileInfo.absoluteFilePath(), false); +} + +void removeLwjglFromPatch(VersionFilePtr patch) +{ + auto filter = [](QList& libs) + { + QList filteredLibs; + for (auto lib : libs) + { + if (!g_VersionFilterData.lwjglWhitelist.contains(lib->artifactPrefix())) + { + filteredLibs.append(lib); + } + } + libs = filteredLibs; + }; + filter(patch->libraries); +} +} diff --git a/ultimmc/launcher/minecraft/ProfileUtils.h b/ultimmc/launcher/minecraft/ProfileUtils.h new file mode 100644 index 0000000..351c36c --- /dev/null +++ b/ultimmc/launcher/minecraft/ProfileUtils.h @@ -0,0 +1,28 @@ +#pragma once +#include "Library.h" +#include "VersionFile.h" + +namespace ProfileUtils +{ +typedef QStringList PatchOrder; + +/// Read and parse a OneSix format order file +bool readOverrideOrders(QString path, PatchOrder &order); + +/// Write a OneSix format order file +bool writeOverrideOrders(QString path, const PatchOrder &order); + + +/// Parse a version file in JSON format +VersionFilePtr parseJsonFile(const QFileInfo &fileInfo, const bool requireOrder); + +/// Save a JSON file (in any format) +bool saveJsonFile(const QJsonDocument doc, const QString & filename); + +/// Parse a version file in binary JSON format +VersionFilePtr parseBinaryJsonFile(const QFileInfo &fileInfo); + +/// Remove LWJGL from a patch file. This is applied to all Mojang-like profile files. +void removeLwjglFromPatch(VersionFilePtr patch); + +} diff --git a/ultimmc/launcher/minecraft/Rule.cpp b/ultimmc/launcher/minecraft/Rule.cpp new file mode 100644 index 0000000..af2861e --- /dev/null +++ b/ultimmc/launcher/minecraft/Rule.cpp @@ -0,0 +1,93 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "Rule.h" + +RuleAction RuleAction_fromString(QString name) +{ + if (name == "allow") + return Allow; + if (name == "disallow") + return Disallow; + return Defer; +} + +QList> rulesFromJsonV4(const QJsonObject &objectWithRules) +{ + QList> rules; + auto rulesVal = objectWithRules.value("rules"); + if (!rulesVal.isArray()) + return rules; + + QJsonArray ruleList = rulesVal.toArray(); + for (auto ruleVal : ruleList) + { + std::shared_ptr rule; + if (!ruleVal.isObject()) + continue; + auto ruleObj = ruleVal.toObject(); + auto actionVal = ruleObj.value("action"); + if (!actionVal.isString()) + continue; + auto action = RuleAction_fromString(actionVal.toString()); + if (action == Defer) + continue; + + auto osVal = ruleObj.value("os"); + if (!osVal.isObject()) + { + // add a new implicit action rule + rules.append(ImplicitRule::create(action)); + continue; + } + + auto osObj = osVal.toObject(); + auto osNameVal = osObj.value("name"); + if (!osNameVal.isString()) + continue; + OpSys requiredOs = OpSys_fromString(osNameVal.toString()); + QString versionRegex = osObj.value("version").toString(); + // add a new OS rule + rules.append(OsRule::create(action, requiredOs, versionRegex)); + } + return rules; +} + +QJsonObject ImplicitRule::toJson() +{ + QJsonObject ruleObj; + ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow")); + return ruleObj; +} + +QJsonObject OsRule::toJson() +{ + QJsonObject ruleObj; + ruleObj.insert("action", m_result == Allow ? QString("allow") : QString("disallow")); + QJsonObject osObj; + { + osObj.insert("name", OpSys_toString(m_system)); + if(!m_version_regexp.isEmpty()) + { + osObj.insert("version", m_version_regexp); + } + } + ruleObj.insert("os", osObj); + return ruleObj; +} + diff --git a/ultimmc/launcher/minecraft/Rule.h b/ultimmc/launcher/minecraft/Rule.h new file mode 100644 index 0000000..7aa34d9 --- /dev/null +++ b/ultimmc/launcher/minecraft/Rule.h @@ -0,0 +1,101 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include "OpSys.h" + +class Library; +class Rule; + +enum RuleAction +{ + Allow, + Disallow, + Defer +}; + +QList> rulesFromJsonV4(const QJsonObject &objectWithRules); + +class Rule +{ +protected: + RuleAction m_result; + virtual bool applies(const Library *parent) = 0; + +public: + Rule(RuleAction result) : m_result(result) + { + } + virtual ~Rule() {}; + virtual QJsonObject toJson() = 0; + RuleAction apply(const Library *parent) + { + if (applies(parent)) + return m_result; + else + return Defer; + } +}; + +class OsRule : public Rule +{ +private: + // the OS + OpSys m_system; + // the OS version regexp + QString m_version_regexp; + +protected: + virtual bool applies(const Library *) + { + return (m_system == currentSystem); + } + OsRule(RuleAction result, OpSys system, QString version_regexp) + : Rule(result), m_system(system), m_version_regexp(version_regexp) + { + } + +public: + virtual QJsonObject toJson(); + static std::shared_ptr create(RuleAction result, OpSys system, + QString version_regexp) + { + return std::shared_ptr(new OsRule(result, system, version_regexp)); + } +}; + +class ImplicitRule : public Rule +{ +protected: + virtual bool applies(const Library *) + { + return true; + } + ImplicitRule(RuleAction result) : Rule(result) + { + } + +public: + virtual QJsonObject toJson(); + static std::shared_ptr create(RuleAction result) + { + return std::shared_ptr(new ImplicitRule(result)); + } +}; diff --git a/ultimmc/launcher/minecraft/VersionFile.cpp b/ultimmc/launcher/minecraft/VersionFile.cpp new file mode 100644 index 0000000..cbe1961 --- /dev/null +++ b/ultimmc/launcher/minecraft/VersionFile.cpp @@ -0,0 +1,60 @@ +#include +#include + +#include + +#include "minecraft/VersionFile.h" +#include "minecraft/Library.h" +#include "minecraft/PackProfile.h" +#include "ParseUtils.h" + +#include + +static bool isMinecraftVersion(const QString &uid) +{ + return uid == "net.minecraft"; +} + +void VersionFile::applyTo(LaunchProfile *profile) +{ + // Only real Minecraft can set those. Don't let anything override them. + if (isMinecraftVersion(uid)) + { + profile->applyMinecraftVersion(version); + profile->applyMinecraftVersionType(type); + // HACK: ignore assets from other version files than Minecraft + // workaround for stupid assets issue caused by amazon: + // https://www.theregister.co.uk/2017/02/28/aws_is_awol_as_s3_goes_haywire/ + profile->applyMinecraftAssets(mojangAssetIndex); + } + + profile->applyMainJar(mainJar); + profile->applyMainClass(mainClass); + profile->applyAppletClass(appletClass); + profile->applyMinecraftArguments(minecraftArguments); + profile->applyTweakers(addTweakers); + profile->applyJarMods(jarMods); + profile->applyMods(mods); + profile->applyTraits(traits); + + for (auto library : libraries) + { + profile->applyLibrary(library); + } + for (auto mavenFile : mavenFiles) + { + profile->applyMavenFile(mavenFile); + } + profile->applyProblemSeverity(getProblemSeverity()); +} + +/* + auto theirVersion = profile->getMinecraftVersion(); + if (!theirVersion.isNull() && !dependsOnMinecraftVersion.isNull()) + { + if (QRegExp(dependsOnMinecraftVersion, Qt::CaseInsensitive, QRegExp::Wildcard).indexIn(theirVersion) == -1) + { + throw MinecraftVersionMismatch(uid, dependsOnMinecraftVersion, theirVersion); + } + } +*/ diff --git a/ultimmc/launcher/minecraft/VersionFile.h b/ultimmc/launcher/minecraft/VersionFile.h new file mode 100644 index 0000000..3141dd5 --- /dev/null +++ b/ultimmc/launcher/minecraft/VersionFile.h @@ -0,0 +1,114 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include "minecraft/OpSys.h" +#include "minecraft/Rule.h" +#include "ProblemProvider.h" +#include "Library.h" +#include + +class PackProfile; +class VersionFile; +class LaunchProfile; +struct MojangDownloadInfo; +struct MojangAssetIndexInfo; + +using VersionFilePtr = std::shared_ptr; +class VersionFile : public ProblemContainer +{ + friend class MojangVersionFormat; + friend class OneSixVersionFormat; +public: /* methods */ + void applyTo(LaunchProfile* profile); + +public: /* data */ + /// MultiMC: order hint for this version file if no explicit order is set + int order = 0; + + /// MultiMC: human readable name of this package + QString name; + + /// MultiMC: package ID of this package + QString uid; + + /// MultiMC: version of this package + QString version; + + /// MultiMC: DEPRECATED dependency on a Minecraft version + QString dependsOnMinecraftVersion; + + /// Mojang: DEPRECATED used to version the Mojang version format + int minimumLauncherVersion = -1; + + /// Mojang: DEPRECATED version of Minecraft this is + QString minecraftVersion; + + /// Mojang: class to launch Minecraft with + QString mainClass; + + /// MultiMC: class to launch legacy Minecraft with (embed in a custom window) + QString appletClass; + + /// Mojang: Minecraft launch arguments (may contain placeholders for variable substitution) + QString minecraftArguments; + + /// Mojang: type of the Minecraft version + QString type; + + /// Mojang: the time this version was actually released by Mojang + QDateTime releaseTime; + + /// Mojang: DEPRECATED the time this version was last updated by Mojang + QDateTime updateTime; + + /// Mojang: DEPRECATED asset group to be used with Minecraft + QString assets; + + /// MultiMC: list of tweaker mod arguments for launchwrapper + QStringList addTweakers; + + /// Mojang: list of libraries to add to the version + QList libraries; + + /// MultiMC: list of maven files to put in the libraries folder, but not in classpath + QList mavenFiles; + + /// The main jar (Minecraft version library, normally) + LibraryPtr mainJar; + + /// MultiMC: list of attached traits of this version file - used to enable features + QSet traits; + + /// MultiMC: list of jar mods added to this version + QList jarMods; + + /// MultiMC: list of mods added to this version + QList mods; + + /** + * MultiMC: set of packages this depends on + * NOTE: this is shared with the meta format!!! + */ + Meta::RequireSet depends; + + /** + * MultiMC: set of packages this conflicts with + * NOTE: this is shared with the meta format!!! + */ + Meta::RequireSet conflicts; + + /// is volatile -- may be removed as soon as it is no longer needed by something else + bool m_volatile = false; + +public: + // Mojang: DEPRECATED list of 'downloads' - client jar, server jar, windows server exe, maybe more. + QMap > mojangDownloads; + + // Mojang: extended asset index download information + std::shared_ptr mojangAssetIndex; +}; diff --git a/ultimmc/launcher/minecraft/VersionFilterData.cpp b/ultimmc/launcher/minecraft/VersionFilterData.cpp new file mode 100644 index 0000000..ca218e4 --- /dev/null +++ b/ultimmc/launcher/minecraft/VersionFilterData.cpp @@ -0,0 +1,77 @@ +#include "VersionFilterData.h" +#include "ParseUtils.h" + +VersionFilterData g_VersionFilterData = VersionFilterData(); + +VersionFilterData::VersionFilterData() +{ + // 1.3.* + auto libs13 = + QList{{"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b"}, + {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f"}, + {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82"}}; + + fmlLibsMapping["1.3.2"] = libs13; + + // 1.4.* + auto libs14 = QList{ + {"argo-2.25.jar", "bb672829fde76cb163004752b86b0484bd0a7f4b"}, + {"guava-12.0.1.jar", "b8e78b9af7bf45900e14c6f958486b6ca682195f"}, + {"asm-all-4.0.jar", "98308890597acb64047f7e896638e0d98753ae82"}, + {"bcprov-jdk15on-147.jar", "b6f5d9926b0afbde9f4dbe3db88c5247be7794bb"}}; + + fmlLibsMapping["1.4"] = libs14; + fmlLibsMapping["1.4.1"] = libs14; + fmlLibsMapping["1.4.2"] = libs14; + fmlLibsMapping["1.4.3"] = libs14; + fmlLibsMapping["1.4.4"] = libs14; + fmlLibsMapping["1.4.5"] = libs14; + fmlLibsMapping["1.4.6"] = libs14; + fmlLibsMapping["1.4.7"] = libs14; + + // 1.5 + fmlLibsMapping["1.5"] = QList{ + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"}, + {"deobfuscation_data_1.5.zip", "5f7c142d53776f16304c0bbe10542014abad6af8"}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}}; + + // 1.5.1 + fmlLibsMapping["1.5.1"] = QList{ + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"}, + {"deobfuscation_data_1.5.1.zip", "22e221a0d89516c1f721d6cab056a7e37471d0a6"}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}}; + + // 1.5.2 + fmlLibsMapping["1.5.2"] = QList{ + {"argo-small-3.2.jar", "58912ea2858d168c50781f956fa5b59f0f7c6b51"}, + {"guava-14.0-rc3.jar", "931ae21fa8014c3ce686aaa621eae565fefb1a6a"}, + {"asm-all-4.1.jar", "054986e962b88d8660ae4566475658469595ef58"}, + {"bcprov-jdk15on-148.jar", "960dea7c9181ba0b17e8bab0c06a43f0a5f04e65"}, + {"deobfuscation_data_1.5.2.zip", "446e55cd986582c70fcf12cb27bc00114c5adfd9"}, + {"scala-library.jar", "458d046151ad179c85429ed7420ffb1eaf6ddf85"}}; + + // don't use installers for those. + forgeInstallerBlacklist = QSet({"1.5.2"}); + + // FIXME: remove, used for deciding when core mods should display + legacyCutoffDate = timeFromS3Time("2013-06-25T15:08:56+02:00"); + lwjglWhitelist = + QSet{"net.java.jinput:jinput", "net.java.jinput:jinput-platform", + "net.java.jutils:jutils", "org.lwjgl.lwjgl:lwjgl", + "org.lwjgl.lwjgl:lwjgl_util", "org.lwjgl.lwjgl:lwjgl-platform"}; + + java8BeginsDate = timeFromS3Time("2017-03-30T09:32:19+00:00"); + java16BeginsDate = timeFromS3Time("2021-05-12T11:19:15+00:00"); + java17BeginsDate = timeFromS3Time("2021-11-16T17:04:48+00:00"); + java21BeginsDate = timeFromS3Time("2024-04-03T11:49:39+00:00"); + quickPlayBeginsDate = timeFromS3Time("2023-04-05T12:05:17+00:00"); + liteLoaderEndsDate = timeFromS3Time("2017-09-18T08:39:46+00:00"); + fabricBeginsDate = timeFromS3Time("2019-04-23T14:52:44+00:00"); + neoForgeBeginsDate = timeFromS3Time("2023-06-12T13:25:51+00:00"); +} diff --git a/ultimmc/launcher/minecraft/VersionFilterData.h b/ultimmc/launcher/minecraft/VersionFilterData.h new file mode 100644 index 0000000..4784822 --- /dev/null +++ b/ultimmc/launcher/minecraft/VersionFilterData.h @@ -0,0 +1,41 @@ +#pragma once +#include +#include +#include +#include + +struct FMLlib +{ + QString filename; + QString checksum; +}; + +struct VersionFilterData +{ + VersionFilterData(); + // mapping between minecraft versions and FML libraries required + QMap> fmlLibsMapping; + // set of minecraft versions for which using forge installers is blacklisted + QSet forgeInstallerBlacklist; + // no new versions below this date will be accepted from Mojang servers + QDateTime legacyCutoffDate; + // Libraries that belong to LWJGL + QSet lwjglWhitelist; + // release date of first version to require Java 8 (17w13a) + QDateTime java8BeginsDate; + // release data of first version to require Java 16 (21w19a) + QDateTime java16BeginsDate; + // release data of first version to require Java 17 (1.18 Pre Release 2) + QDateTime java17BeginsDate; + // Release data of the first version to require java 21 (24w14a) + QDateTime java21BeginsDate; + // release date of first version to use --quickPlayMultiplayer instead of --server/--port for directly joining servers + QDateTime quickPlayBeginsDate; + // release date of last version to support LiteLoader (1.12.2) + QDateTime liteLoaderEndsDate; + // release date of first version supported by Fabric/Quilt (1.14) + QDateTime fabricBeginsDate; + // release date of first version supported by NeoForge (1.20.1) + QDateTime neoForgeBeginsDate; +}; +extern VersionFilterData g_VersionFilterData; diff --git a/ultimmc/launcher/minecraft/World.cpp b/ultimmc/launcher/minecraft/World.cpp new file mode 100644 index 0000000..a2b4dac --- /dev/null +++ b/ultimmc/launcher/minecraft/World.cpp @@ -0,0 +1,520 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include "World.h" + +#include "GZip.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +using nonstd::optional; +using nonstd::nullopt; + +GameType::GameType(nonstd::optional original): + original(original) +{ + if(!original) { + return; + } + switch(*original) { + case 0: + type = GameType::Survival; + break; + case 1: + type = GameType::Creative; + break; + case 2: + type = GameType::Adventure; + break; + case 3: + type = GameType::Spectator; + break; + default: + break; + } +} + +QString GameType::toTranslatedString() const +{ + switch (type) + { + case GameType::Survival: + return QCoreApplication::translate("GameType", "Survival"); + case GameType::Creative: + return QCoreApplication::translate("GameType", "Creative"); + case GameType::Adventure: + return QCoreApplication::translate("GameType", "Adventure"); + case GameType::Spectator: + return QCoreApplication::translate("GameType", "Spectator"); + default: + break; + } + if(original) { + return QCoreApplication::translate("GameType", "Unknown (%1)").arg(*original); + } + return QCoreApplication::translate("GameType", "Undefined"); +} + +QString GameType::toLogString() const +{ + switch (type) + { + case GameType::Survival: + return "Survival"; + case GameType::Creative: + return "Creative"; + case GameType::Adventure: + return "Adventure"; + case GameType::Spectator: + return "Spectator"; + default: + break; + } + if(original) { + return QString("Unknown (%1)").arg(*original); + } + return "Undefined"; +} + +std::unique_ptr parseLevelDat(QByteArray data) +{ + QByteArray output; + if(!GZip::unzip(data, output)) + { + return nullptr; + } + std::istringstream foo(std::string(output.constData(), output.size())); + try { + auto pair = nbt::io::read_compound(foo); + + if(pair.first != "") + return nullptr; + + if(pair.second == nullptr) + return nullptr; + + return std::move(pair.second); + } + catch (const nbt::io::input_error &e) + { + qWarning() << "Unable to parse level.dat:" << e.what(); + return nullptr; + } +} + +QByteArray serializeLevelDat(nbt::tag_compound * levelInfo) +{ + std::ostringstream s; + nbt::io::write_tag("", *levelInfo, s); + QByteArray val( s.str().data(), (int) s.str().size() ); + return val; +} + +QString getLevelDatFromFS(const QFileInfo &file) +{ + QDir worldDir(file.filePath()); + if(!file.isDir() || !worldDir.exists("level.dat")) + { + return QString(); + } + return worldDir.absoluteFilePath("level.dat"); +} + +QByteArray getLevelDatDataFromFS(const QFileInfo &file) +{ + auto fullFilePath = getLevelDatFromFS(file); + if(fullFilePath.isNull()) + { + return QByteArray(); + } + QFile f(fullFilePath); + if(!f.open(QIODevice::ReadOnly)) + { + return QByteArray(); + } + return f.readAll(); +} + +bool putLevelDatDataToFS(const QFileInfo &file, QByteArray & data) +{ + auto fullFilePath = getLevelDatFromFS(file); + if(fullFilePath.isNull()) + { + return false; + } + QSaveFile f(fullFilePath); + if(!f.open(QIODevice::WriteOnly)) + { + return false; + } + QByteArray compressed; + if(!GZip::zip(data, compressed)) + { + return false; + } + if(f.write(compressed) != compressed.size()) + { + f.cancelWriting(); + return false; + } + return f.commit(); +} + +World::World(const QFileInfo &file) +{ + repath(file); +} + +void World::repath(const QFileInfo &file) +{ + m_containerFile = file; + m_folderName = file.fileName(); + if(file.isFile() && file.suffix() == "zip") + { + m_iconFile = QString(); + readFromZip(file); + } + else if(file.isDir()) + { + QFileInfo assumedIconPath(file.absoluteFilePath() + "/icon.png"); + if(assumedIconPath.exists()) { + m_iconFile = assumedIconPath.absoluteFilePath(); + } + readFromFS(file); + } +} + +bool World::resetIcon() +{ + if(m_iconFile.isNull()) { + return false; + } + if(QFile(m_iconFile).remove()) { + m_iconFile = QString(); + return true; + } + return false; +} + +void World::readFromFS(const QFileInfo &file) +{ + auto bytes = getLevelDatDataFromFS(file); + if(bytes.isEmpty()) + { + is_valid = false; + return; + } + loadFromLevelDat(bytes); + levelDatTime = file.lastModified(); +} + +void World::readFromZip(const QFileInfo &file) +{ + QuaZip zip(file.absoluteFilePath()); + is_valid = zip.open(QuaZip::mdUnzip); + if (!is_valid) + { + return; + } + auto location = MMCZip::findFolderOfFileInZip(&zip, "level.dat"); + is_valid = !location.isEmpty(); + if (!is_valid) + { + return; + } + m_containerOffsetPath = location; + QuaZipFile zippedFile(&zip); + // read the install profile + is_valid = zip.setCurrentFile(location + "level.dat"); + if (!is_valid) + { + return; + } + is_valid = zippedFile.open(QIODevice::ReadOnly); + QuaZipFileInfo64 levelDatInfo; + zippedFile.getFileInfo(&levelDatInfo); + auto modTime = levelDatInfo.getNTFSmTime(); + if(!modTime.isValid()) + { + modTime = levelDatInfo.dateTime; + } + levelDatTime = modTime; + if (!is_valid) + { + return; + } + loadFromLevelDat(zippedFile.readAll()); + zippedFile.close(); +} + +bool World::install(const QString &to, const QString &name) +{ + auto finalPath = FS::PathCombine(to, FS::DirNameFromString(m_actualName, to)); + if(!FS::ensureFolderPathExists(finalPath)) + { + return false; + } + bool ok = false; + if(m_containerFile.isFile()) + { + QuaZip zip(m_containerFile.absoluteFilePath()); + if (!zip.open(QuaZip::mdUnzip)) + { + return false; + } + ok = !MMCZip::extractSubDir(&zip, m_containerOffsetPath, finalPath); + } + else if(m_containerFile.isDir()) + { + QString from = m_containerFile.filePath(); + ok = FS::copy(from, finalPath)(); + } + + if(ok && !name.isEmpty() && m_actualName != name) + { + World newWorld(finalPath); + if(newWorld.isValid()) + { + newWorld.rename(name); + } + } + return ok; +} + +bool World::rename(const QString &newName) +{ + if(m_containerFile.isFile()) + { + return false; + } + + auto data = getLevelDatDataFromFS(m_containerFile); + if(data.isEmpty()) + { + return false; + } + + auto worldData = parseLevelDat(data); + if(!worldData) + { + return false; + } + auto &val = worldData->at("Data"); + if(val.get_type() != nbt::tag_type::Compound) + { + return false; + } + auto &dataCompound = val.as(); + dataCompound.put("LevelName", nbt::value_initializer(newName.toUtf8().data())); + data = serializeLevelDat(worldData.get()); + + putLevelDatDataToFS(m_containerFile, data); + + m_actualName = newName; + + QDir parentDir(m_containerFile.absoluteFilePath()); + parentDir.cdUp(); + QFile container(m_containerFile.absoluteFilePath()); + auto dirName = FS::DirNameFromString(m_actualName, parentDir.absolutePath()); + container.rename(parentDir.absoluteFilePath(dirName)); + + return true; +} + +namespace { + +optional read_string (nbt::value& parent, const char * name) +{ + try + { + auto &namedValue = parent.at(name); + if(namedValue.get_type() != nbt::tag_type::String) + { + return nullopt; + } + auto & tag_str = namedValue.as(); + return QString::fromStdString(tag_str.get()); + } + catch (const std::out_of_range &e) + { + // fallback for old world formats + qWarning() << "String NBT tag" << name << "could not be found."; + return nullopt; + } + catch (const std::bad_cast &e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to string."; + return nullopt; + } +} + +optional read_long (nbt::value& parent, const char * name) +{ + try + { + auto &namedValue = parent.at(name); + if(namedValue.get_type() != nbt::tag_type::Long) + { + return nullopt; + } + auto & tag_str = namedValue.as(); + return tag_str.get(); + } + catch (const std::out_of_range &e) + { + // fallback for old world formats + qWarning() << "Long NBT tag" << name << "could not be found."; + return nullopt; + } + catch (const std::bad_cast &e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to long."; + return nullopt; + } +} + +optional read_int (nbt::value& parent, const char * name) +{ + try + { + auto &namedValue = parent.at(name); + if(namedValue.get_type() != nbt::tag_type::Int) + { + return nullopt; + } + auto & tag_str = namedValue.as(); + return tag_str.get(); + } + catch (const std::out_of_range &e) + { + // fallback for old world formats + qWarning() << "Int NBT tag" << name << "could not be found."; + return nullopt; + } + catch (const std::bad_cast &e) + { + // type mismatch + qWarning() << "NBT tag" << name << "could not be converted to int."; + return nullopt; + } +} + +GameType read_gametype(nbt::value& parent, const char * name) { + return GameType(read_int(parent, name)); +} + +} + +void World::loadFromLevelDat(QByteArray data) +{ + auto levelData = parseLevelDat(data); + if(!levelData) + { + is_valid = false; + return; + } + + nbt::value * valPtr = nullptr; + try { + valPtr = &levelData->at("Data"); + } + catch (const std::out_of_range &e) { + qWarning() << "Unable to read NBT tags from " << m_folderName << ":" << e.what(); + is_valid = false; + return; + } + nbt::value &val = *valPtr; + + is_valid = val.get_type() == nbt::tag_type::Compound; + if(!is_valid) + return; + + auto name = read_string(val, "LevelName"); + m_actualName = name ? *name : m_folderName; + + auto timestamp = read_long(val, "LastPlayed"); + m_lastPlayed = timestamp ? QDateTime::fromMSecsSinceEpoch(*timestamp) : levelDatTime; + + m_gameType = read_gametype(val, "GameType"); + + optional randomSeed; + try { + auto &WorldGen_val = val.at("WorldGenSettings"); + randomSeed = read_long(WorldGen_val, "seed"); + } + catch (const std::out_of_range &) {} + if(!randomSeed) { + randomSeed = read_long(val, "RandomSeed"); + } + m_randomSeed = randomSeed ? *randomSeed : 0; + + qDebug() << "World Name:" << m_actualName; + qDebug() << "Last Played:" << m_lastPlayed.toString(); + if(randomSeed) { + qDebug() << "Seed:" << *randomSeed; + } + qDebug() << "GameType:" << m_gameType.toLogString(); +} + +bool World::replace(World &with) +{ + if (!destroy()) + return false; + bool success = FS::copy(with.m_containerFile.filePath(), m_containerFile.path())(); + if (success) + { + m_folderName = with.m_folderName; + m_containerFile.refresh(); + } + return success; +} + +bool World::destroy() +{ + if(!is_valid) return false; + if (m_containerFile.isDir()) + { + QDir d(m_containerFile.filePath()); + return d.removeRecursively(); + } + else if(m_containerFile.isFile()) + { + QFile file(m_containerFile.absoluteFilePath()); + return file.remove(); + } + return true; +} + +bool World::operator==(const World &other) const +{ + return is_valid == other.is_valid && folderName() == other.folderName(); +} diff --git a/ultimmc/launcher/minecraft/World.h b/ultimmc/launcher/minecraft/World.h new file mode 100644 index 0000000..35e3278 --- /dev/null +++ b/ultimmc/launcher/minecraft/World.h @@ -0,0 +1,111 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include + +struct GameType { + GameType() = default; + GameType (nonstd::optional original); + + QString toTranslatedString() const; + QString toLogString() const; + + enum + { + Unknown = -1, + Survival = 0, + Creative, + Adventure, + Spectator + } type = Unknown; + nonstd::optional original; +}; + +class World +{ +public: + World(const QFileInfo &file); + QString folderName() const + { + return m_folderName; + } + QString name() const + { + return m_actualName; + } + QString iconFile() const + { + return m_iconFile; + } + QDateTime lastPlayed() const + { + return m_lastPlayed; + } + GameType gameType() const + { + return m_gameType; + } + int64_t seed() const + { + return m_randomSeed; + } + bool isValid() const + { + return is_valid; + } + bool isOnFS() const + { + return m_containerFile.isDir(); + } + QFileInfo container() const + { + return m_containerFile; + } + // delete all the files of this world + bool destroy(); + // replace this world with a copy of the other + bool replace(World &with); + // change the world's filesystem path (used by world lists for *MAGIC* purposes) + void repath(const QFileInfo &file); + // remove the icon file, if any + bool resetIcon(); + + bool rename(const QString &to); + bool install(const QString &to, const QString &name= QString()); + + // WEAK compare operator - used for replacing worlds + bool operator==(const World &other) const; + +private: + void readFromZip(const QFileInfo &file); + void readFromFS(const QFileInfo &file); + void loadFromLevelDat(QByteArray data); + +protected: + + QFileInfo m_containerFile; + QString m_containerOffsetPath; + QString m_folderName; + QString m_actualName; + QString m_iconFile; + QDateTime levelDatTime; + QDateTime m_lastPlayed; + int64_t m_randomSeed = 0; + GameType m_gameType; + bool is_valid = false; +}; diff --git a/ultimmc/launcher/minecraft/WorldList.cpp b/ultimmc/launcher/minecraft/WorldList.cpp new file mode 100644 index 0000000..dcdbc32 --- /dev/null +++ b/ultimmc/launcher/minecraft/WorldList.cpp @@ -0,0 +1,386 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WorldList.h" +#include +#include +#include +#include +#include +#include +#include + +WorldList::WorldList(const QString &dir) + : QAbstractListModel(), m_dir(dir) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_watcher = new QFileSystemWatcher(this); + is_watching = false; + connect(m_watcher, SIGNAL(directoryChanged(QString)), this, + SLOT(directoryChanged(QString))); +} + +void WorldList::startWatching() +{ + if(is_watching) + { + return; + } + update(); + is_watching = m_watcher->addPath(m_dir.absolutePath()); + if (is_watching) + { + qDebug() << "Started watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void WorldList::stopWatching() +{ + if(!is_watching) + { + return; + } + is_watching = !m_watcher->removePath(m_dir.absolutePath()); + if (!is_watching) + { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +bool WorldList::update() +{ + if (!isValid()) + return false; + + QList newWorlds; + m_dir.refresh(); + auto folderContents = m_dir.entryInfoList(); + // if there are any untracked files... + for (QFileInfo entry : folderContents) + { + if(!entry.isDir()) + continue; + + World w(entry); + if(w.isValid()) + { + newWorlds.append(w); + } + } + beginResetModel(); + worlds.swap(newWorlds); + endResetModel(); + return true; +} + +void WorldList::directoryChanged(QString path) +{ + update(); +} + +bool WorldList::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +bool WorldList::deleteWorld(int index) +{ + if (index >= worlds.size() || index < 0) + return false; + World &m = worlds[index]; + if (m.destroy()) + { + beginRemoveRows(QModelIndex(), index, index); + worlds.removeAt(index); + endRemoveRows(); + emit changed(); + return true; + } + return false; +} + +bool WorldList::deleteWorlds(int first, int last) +{ + for (int i = first; i <= last; i++) + { + World &m = worlds[i]; + m.destroy(); + } + beginRemoveRows(QModelIndex(), first, last); + worlds.erase(worlds.begin() + first, worlds.begin() + last + 1); + endRemoveRows(); + emit changed(); + return true; +} + +bool WorldList::resetIcon(int row) +{ + if (row >= worlds.size() || row < 0) + return false; + World &m = worlds[row]; + if(m.resetIcon()) { + emit dataChanged(index(row), index(row), {WorldList::IconFileRole}); + return true; + } + return false; +} + + +int WorldList::columnCount(const QModelIndex &parent) const +{ + return 3; +} + +QVariant WorldList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= worlds.size()) + return QVariant(); + + auto & world = worlds[row]; + switch (role) + { + case Qt::DisplayRole: + switch (column) + { + case NameColumn: + return world.name(); + + case GameModeColumn: + return world.gameType().toTranslatedString(); + + case LastPlayedColumn: + return world.lastPlayed(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + { + return world.folderName(); + } + case ObjectRole: + { + return QVariant::fromValue((void *)&world); + } + case FolderRole: + { + return QDir::toNativeSeparators(dir().absoluteFilePath(world.folderName())); + } + case SeedRole: + { + return qVariantFromValue(world.seed()); + } + case NameRole: + { + return world.name(); + } + case LastPlayedRole: + { + return world.lastPlayed(); + } + case IconFileRole: + { + return world.iconFile(); + } + default: + return QVariant(); + } +} + +QVariant WorldList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case NameColumn: + return tr("Name"); + case GameModeColumn: + return tr("Game Mode"); + case LastPlayedColumn: + return tr("Last Played"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case NameColumn: + return tr("The name of the world."); + case GameModeColumn: + return tr("Game mode of the world."); + case LastPlayedColumn: + return tr("Date and time the world was last played."); + default: + return QVariant(); + } + default: + return QVariant(); + } + return QVariant(); +} + +QStringList WorldList::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +class WorldMimeData : public QMimeData +{ +Q_OBJECT + +public: + WorldMimeData(QList worlds) + { + m_worlds = worlds; + + } + QStringList formats() const + { + return QMimeData::formats() << "text/uri-list"; + } + +protected: + QVariant retrieveData(const QString &mimetype, QVariant::Type type) const + { + QList urls; + for(auto &world: m_worlds) + { + if(!world.isValid() || !world.isOnFS()) + continue; + QString worldPath = world.container().absoluteFilePath(); + qDebug() << worldPath; + urls.append(QUrl::fromLocalFile(worldPath)); + } + const_cast(this)->setUrls(urls); + return QMimeData::retrieveData(mimetype, type); + } +private: + QList m_worlds; +}; + +QMimeData *WorldList::mimeData(const QModelIndexList &indexes) const +{ + if (indexes.size() == 0) + return new QMimeData(); + + QList worlds; + for(auto idx : indexes) + { + if(idx.column() != 0) + continue; + int row = idx.row(); + if (row < 0 || row >= this->worlds.size()) + continue; + worlds.append(this->worlds[row]); + } + if(!worlds.size()) + { + return new QMimeData(); + } + return new WorldMimeData(worlds); +} + +Qt::ItemFlags WorldList::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + if (index.isValid()) + return Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | + defaultFlags; + else + return Qt::ItemIsDropEnabled | defaultFlags; +} + +Qt::DropActions WorldList::supportedDragActions() const +{ + // move to other mod lists or VOID + return Qt::MoveAction; +} + +Qt::DropActions WorldList::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +void WorldList::installWorld(QFileInfo filename) +{ + qDebug() << "installing: " << filename.absoluteFilePath(); + World w(filename); + if(!w.isValid()) + { + return; + } + w.install(m_dir.absolutePath()); +} + +bool WorldList::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, + const QModelIndex &parent) +{ + if (action == Qt::IgnoreAction) + return true; + // check if the action is supported + if (!data || !(action & supportedDropActions())) + return false; + // files dropped from outside? + if (data->hasUrls()) + { + bool was_watching = is_watching; + if (was_watching) + stopWatching(); + auto urls = data->urls(); + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + continue; + QString filename = url.toLocalFile(); + + QFileInfo worldInfo(filename); + + if(!m_dir.entryInfoList().contains(worldInfo)) + { + installWorld(worldInfo); + } + } + if (was_watching) + startWatching(); + return true; + } + return false; +} + +#include "WorldList.moc" diff --git a/ultimmc/launcher/minecraft/WorldList.h b/ultimmc/launcher/minecraft/WorldList.h new file mode 100644 index 0000000..8e238ee --- /dev/null +++ b/ultimmc/launcher/minecraft/WorldList.h @@ -0,0 +1,129 @@ +/* Copyright 2015-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include "minecraft/World.h" + +class QFileSystemWatcher; + +class WorldList : public QAbstractListModel +{ + Q_OBJECT +public: + enum Columns + { + NameColumn, + GameModeColumn, + LastPlayedColumn + }; + + enum Roles + { + ObjectRole = Qt::UserRole + 1, + FolderRole, + SeedRole, + NameRole, + GameModeRole, + LastPlayedRole, + IconFileRole + }; + + WorldList(const QString &dir); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const + { + return size(); + }; + virtual QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const; + virtual int columnCount(const QModelIndex &parent) const; + + size_t size() const + { + return worlds.size(); + }; + bool empty() const + { + return size() == 0; + } + World &operator[](size_t index) + { + return worlds[index]; + } + + /// Reloads the mod list and returns true if the list changed. + virtual bool update(); + + /// Install a world from location + void installWorld(QFileInfo filename); + + /// Deletes the mod at the given index. + virtual bool deleteWorld(int index); + + /// Removes the world icon, if any + virtual bool resetIcon(int index); + + /// Deletes all the selected mods + virtual bool deleteWorlds(int first, int last); + + /// flags, mostly to support drag&drop + virtual Qt::ItemFlags flags(const QModelIndex &index) const; + /// get data for drag action + virtual QMimeData *mimeData(const QModelIndexList &indexes) const; + /// get the supported mime types + virtual QStringList mimeTypes() const; + /// process data from drop action + virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent); + /// what drag actions do we support? + virtual Qt::DropActions supportedDragActions() const; + + /// what drop actions do we support? + virtual Qt::DropActions supportedDropActions() const; + + void startWatching(); + void stopWatching(); + + virtual bool isValid(); + + QDir dir() const + { + return m_dir; + } + + const QList &allWorlds() const + { + return worlds; + } + +private slots: + void directoryChanged(QString path); + +signals: + void changed(); + +protected: + QFileSystemWatcher *m_watcher; + bool is_watching; + QDir m_dir; + QList worlds; +}; diff --git a/ultimmc/launcher/minecraft/auth/AccountData.cpp b/ultimmc/launcher/minecraft/auth/AccountData.cpp new file mode 100644 index 0000000..3aeaa20 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/AccountData.cpp @@ -0,0 +1,465 @@ +#include "AccountData.h" +#include "AuthProviders.h" +#include +#include +#include +#include +#include + +namespace { +void tokenToJSONV3(QJsonObject &parent, Katabasis::Token t, const char * tokenName) { + if(!t.persistent) { + return; + } + QJsonObject out; + if(t.issueInstant.isValid()) { + out["iat"] = QJsonValue(t.issueInstant.toMSecsSinceEpoch() / 1000); + } + + if(t.notAfter.isValid()) { + out["exp"] = QJsonValue(t.notAfter.toMSecsSinceEpoch() / 1000); + } + + bool save = false; + if(!t.token.isEmpty()) { + out["token"] = QJsonValue(t.token); + save = true; + } + if(!t.refresh_token.isEmpty()) { + out["refresh_token"] = QJsonValue(t.refresh_token); + save = true; + } + if(t.extra.size()) { + out["extra"] = QJsonObject::fromVariantMap(t.extra); + save = true; + } + if(save) { + parent[tokenName] = out; + } +} + +Katabasis::Token tokenFromJSONV3(const QJsonObject &parent, const char * tokenName) { + Katabasis::Token out; + auto tokenObject = parent.value(tokenName).toObject(); + if(tokenObject.isEmpty()) { + return out; + } + auto issueInstant = tokenObject.value("iat"); + if(issueInstant.isDouble()) { + out.issueInstant = QDateTime::fromMSecsSinceEpoch(((int64_t) issueInstant.toDouble()) * 1000); + } + + auto notAfter = tokenObject.value("exp"); + if(notAfter.isDouble()) { + out.notAfter = QDateTime::fromMSecsSinceEpoch(((int64_t) notAfter.toDouble()) * 1000); + } + + auto token = tokenObject.value("token"); + if(token.isString()) { + out.token = token.toString(); + out.validity = Katabasis::Validity::Assumed; + } + + auto refresh_token = tokenObject.value("refresh_token"); + if(refresh_token.isString()) { + out.refresh_token = refresh_token.toString(); + } + + auto extra = tokenObject.value("extra"); + if(extra.isObject()) { + out.extra = extra.toObject().toVariantMap(); + } + return out; +} + +void profileToJSONV3(QJsonObject &parent, MinecraftProfile p, const char * tokenName) { + if(p.id.isEmpty()) { + return; + } + QJsonObject out; + out["id"] = QJsonValue(p.id); + out["name"] = QJsonValue(p.name); + if(!p.currentCape.isEmpty()) { + out["cape"] = p.currentCape; + } + + { + QJsonObject skinObj; + skinObj["id"] = p.skin.id; + skinObj["url"] = p.skin.url; + skinObj["variant"] = p.skin.variant; + if(p.skin.data.size()) { + skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64()); + } + out["skin"] = skinObj; + } + + QJsonArray capesArray; + for(auto & cape: p.capes) { + QJsonObject capeObj; + capeObj["id"] = cape.id; + capeObj["url"] = cape.url; + capeObj["alias"] = cape.alias; + if(cape.data.size()) { + capeObj["data"] = QString::fromLatin1(cape.data.toBase64()); + } + capesArray.push_back(capeObj); + } + out["capes"] = capesArray; + parent[tokenName] = out; +} + +MinecraftProfile profileFromJSONV3(const QJsonObject &parent, const char * tokenName) { + MinecraftProfile out; + auto tokenObject = parent.value(tokenName).toObject(); + if(tokenObject.isEmpty()) { + return out; + } + { + auto idV = tokenObject.value("id"); + auto nameV = tokenObject.value("name"); + if(!idV.isString() || !nameV.isString()) { + qWarning() << "mandatory profile attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + out.name = nameV.toString(); + out.id = idV.toString(); + } + + { + auto skinV = tokenObject.value("skin"); + if(!skinV.isObject()) { + qWarning() << "skin is missing"; + return MinecraftProfile(); + } + auto skinObj = skinV.toObject(); + auto idV = skinObj.value("id"); + auto urlV = skinObj.value("url"); + auto variantV = skinObj.value("variant"); + if(!idV.isString() || !urlV.isString() || !variantV.isString()) { + qWarning() << "mandatory skin attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + out.skin.id = idV.toString(); + out.skin.url = urlV.toString(); + out.skin.variant = variantV.toString(); + + // data for skin is optional + auto dataV = skinObj.value("data"); + if(dataV.isString()) { + // TODO: validate base64 + out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1()); + } + else if (!dataV.isUndefined()) { + qWarning() << "skin data is something unexpected"; + return MinecraftProfile(); + } + } + + { + auto capesV = tokenObject.value("capes"); + if(!capesV.isArray()) { + qWarning() << "capes is not an array!"; + return MinecraftProfile(); + } + auto capesArray = capesV.toArray(); + for(auto capeV: capesArray) { + if(!capeV.isObject()) { + qWarning() << "cape is not an object!"; + return MinecraftProfile(); + } + auto capeObj = capeV.toObject(); + auto idV = capeObj.value("id"); + auto urlV = capeObj.value("url"); + auto aliasV = capeObj.value("alias"); + if(!idV.isString() || !urlV.isString() || !aliasV.isString()) { + qWarning() << "mandatory skin attributes are missing or of unexpected type"; + return MinecraftProfile(); + } + Cape cape; + cape.id = idV.toString(); + cape.url = urlV.toString(); + cape.alias = aliasV.toString(); + + // data for cape is optional. + auto dataV = capeObj.value("data"); + if(dataV.isString()) { + // TODO: validate base64 + cape.data = QByteArray::fromBase64(dataV.toString().toLatin1()); + } + else if (!dataV.isUndefined()) { + qWarning() << "cape data is something unexpected"; + return MinecraftProfile(); + } + out.capes[cape.id] = cape; + } + } + // current cape + { + auto capeV = tokenObject.value("cape"); + if(capeV.isString()) { + auto currentCape = capeV.toString(); + if(out.capes.contains(currentCape)) { + out.currentCape = currentCape; + } + } + } + out.validity = Katabasis::Validity::Assumed; + return out; +} + +void entitlementToJSONV3(QJsonObject &parent, MinecraftEntitlement p) { + if(p.validity == Katabasis::Validity::None) { + return; + } + QJsonObject out; + out["ownsMinecraft"] = QJsonValue(p.ownsMinecraft); + out["canPlayMinecraft"] = QJsonValue(p.canPlayMinecraft); + parent["entitlement"] = out; +} + +bool entitlementFromJSONV3(const QJsonObject &parent, MinecraftEntitlement & out) { + auto entitlementObject = parent.value("entitlement").toObject(); + if(entitlementObject.isEmpty()) { + return false; + } + { + auto ownsMinecraftV = entitlementObject.value("ownsMinecraft"); + auto canPlayMinecraftV = entitlementObject.value("canPlayMinecraft"); + if(!ownsMinecraftV.isBool() || !canPlayMinecraftV.isBool()) { + qWarning() << "mandatory attributes are missing or of unexpected type"; + return false; + } + out.canPlayMinecraft = canPlayMinecraftV.toBool(false); + out.ownsMinecraft = ownsMinecraftV.toBool(false); + out.validity = Katabasis::Validity::Assumed; + } + return true; +} + +} + +bool AccountData::resumeStateFromV2(QJsonObject data) { + // The JSON object must at least have a username for it to be valid. + if (!data.value("username").isString()) + { + qCritical() << "Can't load Mojang account info from JSON object. Username field is missing or of the wrong type."; + return false; + } + + QString userName = data.value("username").toString(""); + QString clientToken = data.value("clientToken").toString(""); + QString accessToken = data.value("accessToken").toString(""); + + QJsonArray profileArray = data.value("profiles").toArray(); + if (profileArray.size() < 1) + { + qCritical() << "Can't load Mojang account with username \"" << userName << "\". No profiles found."; + return false; + } + + struct AccountProfile + { + QString id; + QString name; + bool legacy; + }; + + QList profiles; + int currentProfileIndex = 0; + int index = -1; + QString currentProfile = data.value("activeProfile").toString(""); + for (QJsonValue profileVal : profileArray) + { + index++; + QJsonObject profileObject = profileVal.toObject(); + QString id = profileObject.value("id").toString(""); + QString name = profileObject.value("name").toString(""); + bool legacy = profileObject.value("legacy").toBool(false); + if (id.isEmpty() || name.isEmpty()) + { + qWarning() << "Unable to load a profile" << name << "because it was missing an ID or a name."; + continue; + } + if(id == currentProfile) { + currentProfileIndex = index; + } + profiles.append({id, name, legacy}); + } + auto & profile = profiles[currentProfileIndex]; + + type = AccountType::Mojang; + legacy = profile.legacy; + + minecraftProfile.id = profile.id; + minecraftProfile.name = profile.name; + minecraftProfile.validity = Katabasis::Validity::Assumed; + + yggdrasilToken.token = accessToken; + yggdrasilToken.extra["clientToken"] = clientToken; + yggdrasilToken.extra["userName"] = userName; + yggdrasilToken.validity = Katabasis::Validity::Assumed; + + validity_ = minecraftProfile.validity; + return true; +} + +bool AccountData::resumeStateFromV3(QJsonObject data) { + auto typeV = data.value("type"); + if(!typeV.isString()) { + qWarning() << "Failed to parse account data: type is missing."; + return false; + } + auto typeS = typeV.toString(); + if(typeS == "MSA") { + type = AccountType::MSA; + provider = AuthProviders::lookup("MSA"); + } else if (typeS == "Mojang"){ + type = AccountType::Mojang; + provider = AuthProviders::lookup("mojang"); + } else if (typeS == "Local") { + type = AccountType::Local; + provider = AuthProviders::lookup("local"); + } else if (typeS == "Elyby") { + type = AccountType::Elyby; + provider = AuthProviders::lookup("elyby"); + } else { + qWarning() << "Failed to parse account data: type is not recognized."; + return false; + } + + if(type == AccountType::Mojang) { + legacy = data.value("legacy").toBool(false); + canMigrateToMSA = data.value("canMigrateToMSA").toBool(false); + mustMigrateToMSA = data.value("mustMigrateToMSA").toBool(false); + } + + if(type == AccountType::MSA) { + msaToken = tokenFromJSONV3(data, "msa"); + userToken = tokenFromJSONV3(data, "utoken"); + xboxApiToken = tokenFromJSONV3(data, "xrp-main"); + mojangservicesToken = tokenFromJSONV3(data, "xrp-mc"); + } + + yggdrasilToken = tokenFromJSONV3(data, "ygg"); + minecraftProfile = profileFromJSONV3(data, "profile"); + if(!entitlementFromJSONV3(data, minecraftEntitlement)) { + if(minecraftProfile.validity != Katabasis::Validity::None) { + minecraftEntitlement.canPlayMinecraft = true; + minecraftEntitlement.ownsMinecraft = true; + minecraftEntitlement.validity = Katabasis::Validity::Assumed; + } + } + + validity_ = minecraftProfile.validity; + return true; +} + +QJsonObject AccountData::saveState() const { + QJsonObject output; + if(type == AccountType::Mojang) { + output["type"] = "Mojang"; + if(legacy) { + output["legacy"] = true; + } + if(canMigrateToMSA) { + output["canMigrateToMSA"] = true; + } + if(mustMigrateToMSA) { + output["mustMigrateToMSA"] = true; + } + } + else if (type == AccountType::MSA) { + output["type"] = "MSA"; + tokenToJSONV3(output, msaToken, "msa"); + tokenToJSONV3(output, userToken, "utoken"); + tokenToJSONV3(output, xboxApiToken, "xrp-main"); + tokenToJSONV3(output, mojangservicesToken, "xrp-mc"); + } else if (type == AccountType::Local) { + output["type"] = "Local"; + } else if (type == AccountType::Elyby) { + output["type"] = "Elyby"; + } + + tokenToJSONV3(output, yggdrasilToken, "ygg"); + profileToJSONV3(output, minecraftProfile, "profile"); + entitlementToJSONV3(output, minecraftEntitlement); + return output; +} + +QString AccountData::userName() const { + if(type != AccountType::Mojang && type != AccountType::Elyby) { + return QString(); + } + return yggdrasilToken.extra["userName"].toString(); +} + +QString AccountData::accessToken() const { + return yggdrasilToken.token; +} + +QString AccountData::clientToken() const { + if(type != AccountType::Mojang && type != AccountType::Elyby) { + return QString(); + } + return yggdrasilToken.extra["clientToken"].toString(); +} + +void AccountData::setClientToken(QString clientToken) { + if(type != AccountType::Mojang && type != AccountType::Elyby) { + return; + } + yggdrasilToken.extra["clientToken"] = clientToken; +} + +void AccountData::generateClientTokenIfMissing() { + if(yggdrasilToken.extra.contains("clientToken")) { + return; + } + invalidateClientToken(); +} + +void AccountData::invalidateClientToken() { + if(type != AccountType::Mojang && type != AccountType::Elyby) { + return; + } + yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{-}]")); +} + +QString AccountData::profileId() const { + return minecraftProfile.id; +} + +QString AccountData::profileName() const { + if(minecraftProfile.name.size() == 0) { + return QObject::tr("No profile (%1)").arg(accountDisplayString()); + } + else { + return minecraftProfile.name; + } +} + +QString AccountData::accountDisplayString() const { + switch(type) { + case AccountType::Mojang: + case AccountType::Elyby: { + return userName(); + } + case AccountType::MSA: { + if(xboxApiToken.extra.contains("gtg")) { + return xboxApiToken.extra["gtg"].toString(); + } + return "Xbox profile missing"; + } + case AccountType::Local: { + return ""; + } + default: { + return "Invalid Account"; + } + } +} + +QString AccountData::lastError() const { + return errorString; +} diff --git a/ultimmc/launcher/minecraft/auth/AccountData.h b/ultimmc/launcher/minecraft/auth/AccountData.h new file mode 100644 index 0000000..6b68d8c --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/AccountData.h @@ -0,0 +1,105 @@ +#pragma once +#include +#include +#include +#include +#include + +#include "providers/BaseAuthProvider.h" + +struct Skin { + QString id; + QString url; + QString variant; + + QByteArray data; +}; + +struct Cape { + QString id; + QString url; + QString alias; + + QByteArray data; +}; + +struct MinecraftEntitlement { + bool ownsMinecraft = false; + bool canPlayMinecraft = false; + Katabasis::Validity validity = Katabasis::Validity::None; +}; + +struct MinecraftProfile { + QString id; + QString name; + Skin skin; + QString currentCape; + QMap capes; + Katabasis::Validity validity = Katabasis::Validity::None; +}; + +enum class AccountType { + MSA, + Mojang, + Local, + Elyby +}; + +enum class AccountState { + Unchecked, + Offline, + Working, + Online, + Errored, + Expired, + Gone, + MustMigrate +}; + +struct AccountData { + QJsonObject saveState() const; + bool resumeStateFromV2(QJsonObject data); + bool resumeStateFromV3(QJsonObject data); + + AuthProviderPtr provider; + + //! userName for Mojang accounts, gamertag for MSA + QString accountDisplayString() const; + + //! Only valid for Mojang accounts. MSA does not preserve this information + QString userName() const; + + //! Only valid for Mojang accounts. + QString clientToken() const; + void setClientToken(QString clientToken); + void invalidateClientToken(); + void generateClientTokenIfMissing(); + + //! Yggdrasil access token, as passed to the game. + QString accessToken() const; + + QString profileId() const; + QString profileName() const; + + QString lastError() const; + + AccountType type = AccountType::MSA; + bool legacy = false; + bool canMigrateToMSA = false; + bool mustMigrateToMSA = false; + + Katabasis::Token msaToken; + Katabasis::Token userToken; + Katabasis::Token xboxApiToken; + Katabasis::Token mojangservicesToken; + + Katabasis::Token yggdrasilToken; + MinecraftProfile minecraftProfile; + MinecraftEntitlement minecraftEntitlement; + Katabasis::Validity validity_ = Katabasis::Validity::None; + + // runtime only information (not saved with the account) + QString internalId; + QString errorString; + AccountState accountState = AccountState::Unchecked; +}; diff --git a/ultimmc/launcher/minecraft/auth/AccountList.cpp b/ultimmc/launcher/minecraft/auth/AccountList.cpp new file mode 100644 index 0000000..90efc3b --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/AccountList.cpp @@ -0,0 +1,742 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AccountList.h" +#include "AccountData.h" +#include "AccountTask.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include + +enum AccountListVersion { + MojangOnly = 2, + MojangMSA = 3 +}; + +AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) { + m_refreshTimer = new QTimer(this); + m_refreshTimer->setSingleShot(true); + connect(m_refreshTimer, &QTimer::timeout, this, &AccountList::fillQueue); + m_nextTimer = new QTimer(this); + m_nextTimer->setSingleShot(true); + connect(m_nextTimer, &QTimer::timeout, this, &AccountList::tryNext); +} + +AccountList::~AccountList() noexcept {} + +int AccountList::findAccountByProfileId(const QString& profileId) const { + for (int i = 0; i < count(); i++) { + MinecraftAccountPtr account = at(i); + if (account->profileId() == profileId) { + return i; + } + } + return -1; +} + +MinecraftAccountPtr AccountList::getAccountByProfileName(const QString& profileName) const { + for (int i = 0; i < count(); i++) { + MinecraftAccountPtr account = at(i); + if (account->profileName() == profileName) { + return account; + } + } + return nullptr; +} + +const MinecraftAccountPtr AccountList::at(int i) const +{ + return MinecraftAccountPtr(m_accounts.at(i)); +} + +QStringList AccountList::profileNames() const { + QStringList out; + for(auto & account: m_accounts) { + auto profileName = account->profileName(); + if(profileName.isEmpty()) { + continue; + } + out.append(profileName); + } + return out; +} + +void AccountList::addAccount(const MinecraftAccountPtr account) +{ + // NOTE: Do not allow adding something that's already there + if(m_accounts.contains(account)) { + return; + } + + // hook up notifications for changes in the account + connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); + connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); + + // override/replace existing account with the same profileId + auto profileId = account->profileId(); + if(profileId.size()) { + auto existingAccount = findAccountByProfileId(profileId); + if(existingAccount != -1) { + MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount]; + m_accounts[existingAccount] = account; + if(m_defaultAccount == existingAccountPtr) { + m_defaultAccount = account; + } + // disconnect notifications for changes in the account being replaced + existingAccountPtr->disconnect(this); + emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1)); + onListChanged(); + return; + } + } + + // if we don't have this profileId yet, add the account to the end + int row = m_accounts.count(); + beginInsertRows(QModelIndex(), row, row); + m_accounts.append(account); + endInsertRows(); + onListChanged(); +} + +void AccountList::removeAccount(QModelIndex index) +{ + int row = index.row(); + if(index.isValid() && row >= 0 && row < m_accounts.size()) + { + auto & account = m_accounts[row]; + if(account == m_defaultAccount) + { + m_defaultAccount = nullptr; + onDefaultAccountChanged(); + } + account->disconnect(this); + + beginRemoveRows(QModelIndex(), row, row); + m_accounts.removeAt(index.row()); + endRemoveRows(); + onListChanged(); + } +} + +MinecraftAccountPtr AccountList::defaultAccount() const +{ + return m_defaultAccount; +} + +void AccountList::setDefaultAccount(MinecraftAccountPtr newAccount) +{ + if (!newAccount && m_defaultAccount) + { + int idx = 0; + auto previousDefaultAccount = m_defaultAccount; + m_defaultAccount = nullptr; + for (MinecraftAccountPtr account : m_accounts) + { + if (account == previousDefaultAccount) + { + emit dataChanged(index(idx), index(idx, columnCount(QModelIndex()) - 1)); + } + idx ++; + } + onDefaultAccountChanged(); + } + else + { + auto currentDefaultAccount = m_defaultAccount; + int currentDefaultAccountIdx = -1; + auto newDefaultAccount = m_defaultAccount; + int newDefaultAccountIdx = -1; + int idx = 0; + for (MinecraftAccountPtr account : m_accounts) + { + if (account == newAccount) + { + newDefaultAccount = account; + newDefaultAccountIdx = idx; + } + if(currentDefaultAccount == account) + { + currentDefaultAccountIdx = idx; + } + idx++; + } + if(currentDefaultAccount != newDefaultAccount) + { + emit dataChanged(index(currentDefaultAccountIdx), index(currentDefaultAccountIdx, columnCount(QModelIndex()) - 1)); + emit dataChanged(index(newDefaultAccountIdx), index(newDefaultAccountIdx, columnCount(QModelIndex()) - 1)); + m_defaultAccount = newDefaultAccount; + onDefaultAccountChanged(); + } + } +} + +void AccountList::accountChanged() +{ + // the list changed. there is no doubt. + onListChanged(); +} + +void AccountList::accountActivityChanged(bool active) +{ + MinecraftAccount *account = qobject_cast(sender()); + bool found = false; + for (int i = 0; i < count(); i++) { + if (at(i).get() == account) { + emit dataChanged(index(i), index(i, columnCount(QModelIndex()) - 1)); + found = true; + break; + } + } + if(found) { + emit listActivityChanged(); + if(active) { + beginActivity(); + } + else { + endActivity(); + } + } +} + + +void AccountList::onListChanged() +{ + if (m_autosave) + // TODO: Alert the user if this fails. + saveList(); + + emit listChanged(); +} + +void AccountList::onDefaultAccountChanged() +{ + if (m_autosave) + saveList(); + + emit defaultAccountChanged(); +} + +int AccountList::count() const +{ + return m_accounts.count(); +} + +QVariant AccountList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + MinecraftAccountPtr account = at(index.row()); + + switch (role) + { + case Qt::DisplayRole: + switch (index.column()) + { + case NameColumn: + return account->accountDisplayString(); + + case TypeColumn: { + auto typeStr = account->typeString(); + typeStr[0] = typeStr[0].toUpper(); + return typeStr; + } + + case StatusColumn: { + switch(account->accountState()) { + case AccountState::Unchecked: { + return tr("Unchecked", "Account status"); + } + case AccountState::Offline: { + return tr("Offline", "Account status"); + } + case AccountState::Online: { + return tr("Online", "Account status"); + } + case AccountState::Working: { + return tr("Working", "Account status"); + } + case AccountState::Errored: { + return tr("Errored", "Account status"); + } + case AccountState::Expired: { + return tr("Expired", "Account status"); + } + case AccountState::Gone: { + return tr("Gone", "Account status"); + } + case AccountState::MustMigrate: { + return tr("Must Migrate", "Account status"); + } + } + } + + case ProfileNameColumn: { + return account->profileName(); + } + + case MigrationColumn: { + if(account->isMSA()) { + return tr("N/A", "Can Migrate?"); + } + if (account->canMigrate()) { + return tr("Yes", "Can Migrate?"); + } + else { + return tr("No", "Can Migrate?"); + } + } + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return account->accountDisplayString(); + + case PointerRole: + return QVariant::fromValue(account); + + case Qt::CheckStateRole: + switch (index.column()) + { + case NameColumn: + return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked; + } + + default: + return QVariant(); + } +} + +QVariant AccountList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case NameColumn: + return tr("Account"); + case TypeColumn: + return tr("Type"); + case StatusColumn: + return tr("Status"); + case MigrationColumn: + return tr("Can Migrate?"); + case ProfileNameColumn: + return tr("Profile"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case NameColumn: + return tr("User name of the account."); + case TypeColumn: + return tr("Type of the account - Mojang or MSA."); + case StatusColumn: + return tr("Current status of the account."); + case MigrationColumn: + return tr("Can this account migrate to Microsoft account?"); + case ProfileNameColumn: + return tr("Name of the Minecraft profile associated with the account."); + default: + return QVariant(); + } + + default: + return QVariant(); + } +} + +int AccountList::rowCount(const QModelIndex &) const +{ + // Return count + return count(); +} + +int AccountList::columnCount(const QModelIndex &) const +{ + return NUM_COLUMNS; +} + +Qt::ItemFlags AccountList::flags(const QModelIndex &index) const +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return Qt::NoItemFlags; + } + + return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable; +} + +bool AccountList::setData(const QModelIndex &idx, const QVariant &value, int role) +{ + if (idx.row() < 0 || idx.row() >= rowCount(idx) || !idx.isValid()) + { + return false; + } + + if(role == Qt::CheckStateRole) + { + if(value == Qt::Checked) + { + MinecraftAccountPtr account = at(idx.row()); + setDefaultAccount(account); + } + } + + emit dataChanged(idx, index(idx.row(), columnCount(QModelIndex()) - 1)); + return true; +} + +bool AccountList::loadList() +{ + if (m_listFilePath.isEmpty()) + { + qCritical() << "Can't load Mojang account list. No file path given and no default set."; + return false; + } + + QFile file(m_listFilePath); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::ReadOnly)) + { + qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8(); + return false; + } + + // Read the file and close it. + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); + + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) + { + qCritical() << QString("Failed to parse account list file: %1 at offset %2") + .arg(parseError.errorString(), QString::number(parseError.offset)) + .toUtf8(); + return false; + } + + // Make sure the root is an object. + if (!jsonDoc.isObject()) + { + qCritical() << "Invalid account list JSON: Root should be an array."; + return false; + } + + QJsonObject root = jsonDoc.object(); + + // Make sure the format version matches. + auto listVersion = root.value("formatVersion").toVariant().toInt(); + switch(listVersion) { + case AccountListVersion::MojangOnly: { + return loadV2(root); + } + break; + case AccountListVersion::MojangMSA: { + return loadV3(root); + } + break; + default: { + QString newName = "accounts-old.json"; + qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName; + // Attempt to rename the old version. + file.rename(newName); + return false; + } + } +} + +bool AccountList::loadV2(QJsonObject& root) { + beginResetModel(); + auto defaultUserName = root.value("activeAccount").toString(""); + QJsonArray accounts = root.value("accounts").toArray(); + for (QJsonValue accountVal : accounts) + { + QJsonObject accountObj = accountVal.toObject(); + MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV2(accountObj); + if (account.get() != nullptr) + { + auto profileId = account->profileId(); + if(!profileId.size()) { + continue; + } + if(findAccountByProfileId(profileId) != -1) { + continue; + } + connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); + connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); + m_accounts.append(account); + if (defaultUserName.size() && account->mojangUserName() == defaultUserName) { + m_defaultAccount = account; + } + } + else + { + qWarning() << "Failed to load an account."; + } + } + endResetModel(); + return true; +} + +bool AccountList::loadV3(QJsonObject& root) { + beginResetModel(); + QJsonArray accounts = root.value("accounts").toArray(); + for (QJsonValue accountVal : accounts) + { + QJsonObject accountObj = accountVal.toObject(); + MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV3(accountObj); + if (account.get() != nullptr) + { + auto profileId = account->profileId(); + if(profileId.size()) { + if(findAccountByProfileId(profileId) != -1) { + continue; + } + } + connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged); + connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged); + m_accounts.append(account); + if(accountObj.value("active").toBool(false)) { + m_defaultAccount = account; + } + } + else + { + qWarning() << "Failed to load an account."; + } + } + endResetModel(); + return true; +} + + +bool AccountList::saveList() +{ + if (m_listFilePath.isEmpty()) + { + qCritical() << "Can't save Mojang account list. No file path given and no default set."; + return false; + } + + // make sure the parent folder exists + if(!FS::ensureFilePathExists(m_listFilePath)) + return false; + + // make sure the file wasn't overwritten with a folder before (fixes a bug) + QFileInfo finfo(m_listFilePath); + if(finfo.isDir()) + { + QDir badDir(m_listFilePath); + badDir.removeRecursively(); + } + + qDebug() << "Writing account list to" << m_listFilePath; + + qDebug() << "Building JSON data structure."; + // Build the JSON document to write to the list file. + QJsonObject root; + + root.insert("formatVersion", AccountListVersion::MojangMSA); + + // Build a list of accounts. + qDebug() << "Building account array."; + QJsonArray accounts; + for (MinecraftAccountPtr account : m_accounts) + { + QJsonObject accountObj = account->saveToJson(); + if(m_defaultAccount == account) { + accountObj["active"] = true; + } + accounts.append(accountObj); + } + + // Insert the account list into the root object. + root.insert("accounts", accounts); + + // Create a JSON document object to convert our JSON to bytes. + QJsonDocument doc(root); + + // Now that we're done building the JSON object, we can write it to the file. + qDebug() << "Writing account list to file."; + QSaveFile file(m_listFilePath); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::WriteOnly)) + { + qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8(); + return false; + } + + // Write the JSON to the file. + file.write(doc.toJson()); + file.setPermissions(QFile::ReadOwner|QFile::WriteOwner|QFile::ReadUser|QFile::WriteUser); + if(file.commit()) { + qDebug() << "Saved account list to" << m_listFilePath; + return true; + } + else { + qDebug() << "Failed to save accounts to" << m_listFilePath; + return false; + } +} + +void AccountList::setListFilePath(QString path, bool autosave) +{ + m_listFilePath = path; + m_autosave = autosave; +} + +bool AccountList::anyAccountIsValid() +{ + for(auto account: m_accounts) + { + if(account->ownsMinecraft()) { + return true; + } + } + return false; +} + +void AccountList::fillQueue() { + + if(m_defaultAccount && m_defaultAccount->shouldRefresh()) { + auto idToRefresh = m_defaultAccount->internalId(); + m_refreshQueue.push_back(idToRefresh); + qDebug() << "AccountList: Queued default account with internal ID " << idToRefresh << " to refresh first"; + } + + for(int i = 0; i < count(); i++) { + auto account = at(i); + if(account == m_defaultAccount) { + continue; + } + + if(account->shouldRefresh()) { + auto idToRefresh = account->internalId(); + queueRefresh(idToRefresh); + } + } + tryNext(); +} + +void AccountList::requestRefresh(QString accountId) { + auto index = m_refreshQueue.indexOf(accountId); + if(index != -1) { + m_refreshQueue.removeAt(index); + } + m_refreshQueue.push_front(accountId); + qDebug() << "AccountList: Pushed account with internal ID " << accountId << " to the front of the queue"; + if(!isActive()) { + tryNext(); + } +} + +void AccountList::queueRefresh(QString accountId) { + if(m_refreshQueue.indexOf(accountId) != -1) { + return; + } + m_refreshQueue.push_back(accountId); + qDebug() << "AccountList: Queued account with internal ID " << accountId << " to refresh"; +} + + +void AccountList::tryNext() { + while (m_refreshQueue.length()) { + auto accountId = m_refreshQueue.front(); + m_refreshQueue.pop_front(); + for(int i = 0; i < count(); i++) { + auto account = at(i); + if(account->internalId() == accountId) { + m_currentTask = account->refresh(); + if(m_currentTask) { + connect(m_currentTask.get(), &AccountTask::succeeded, this, &AccountList::authSucceeded); + connect(m_currentTask.get(), &AccountTask::failed, this, &AccountList::authFailed); + m_currentTask->start(); + qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID " << accountId; + return; + } + } + } + qDebug() << "RefreshSchedule: Account with with internal ID " << accountId << " not found."; + } + // if we get here, no account needed refreshing. Schedule refresh in an hour. + m_refreshTimer->start(1000 * 3600); +} + +void AccountList::authSucceeded() { + qDebug() << "RefreshSchedule: Background account refresh succeeded"; + m_currentTask.reset(); + m_nextTimer->start(1000 * 20); +} + +void AccountList::authFailed(QString reason) { + qDebug() << "RefreshSchedule: Background account refresh failed: " << reason; + m_currentTask.reset(); + m_nextTimer->start(1000 * 20); +} + +bool AccountList::isActive() const { + return m_activityCount != 0; +} + +void AccountList::beginActivity() { + bool activating = m_activityCount == 0; + m_activityCount++; + if(activating) { + emit activityChanged(true); + } +} + +void AccountList::endActivity() { + if(m_activityCount == 0) { + qWarning() << m_name << " - Activity count would become below zero"; + return; + } + bool deactivating = m_activityCount == 1; + m_activityCount--; + if(deactivating) { + emit activityChanged(false); + } +} diff --git a/ultimmc/launcher/minecraft/auth/AccountList.h b/ultimmc/launcher/minecraft/auth/AccountList.h new file mode 100644 index 0000000..fa1e743 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/AccountList.h @@ -0,0 +1,160 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "MinecraftAccount.h" + +#include +#include +#include +#include + +/*! + * List of available Mojang accounts. + * This should be loaded in the background by MultiMC on startup. + */ +class AccountList : public QAbstractListModel +{ + Q_OBJECT +public: + enum ModelRoles + { + PointerRole = 0x34B1CB48 + }; + + enum VListColumns + { + // TODO: Add icon column. + NameColumn = 0, + ProfileNameColumn, + MigrationColumn, + TypeColumn, + StatusColumn, + + NUM_COLUMNS + }; + + explicit AccountList(QObject *parent = 0); + virtual ~AccountList() noexcept; + + const MinecraftAccountPtr at(int i) const; + int count() const; + + //////// List Model Functions //////// + QVariant data(const QModelIndex &index, int role) const override; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + virtual int rowCount(const QModelIndex &parent) const override; + virtual int columnCount(const QModelIndex &parent) const override; + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + virtual bool setData(const QModelIndex &index, const QVariant &value, int role) override; + + void addAccount(const MinecraftAccountPtr account); + void removeAccount(QModelIndex index); + int findAccountByProfileId(const QString &profileId) const; + MinecraftAccountPtr getAccountByProfileName(const QString &profileName) const; + QStringList profileNames() const; + + // requesting a refresh pushes it to the front of the queue + void requestRefresh(QString accountId); + // queuing a refresh will let it go to the back of the queue (unless it's somewhere inside the queue already) + void queueRefresh(QString accountId); + + /*! + * Sets the path to load/save the list file from/to. + * If autosave is true, this list will automatically save to the given path whenever it changes. + * THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately + * after calling this function to ensure an autosaved change doesn't overwrite the list you intended + * to load. + */ + void setListFilePath(QString path, bool autosave = false); + + bool loadList(); + bool loadV2(QJsonObject &root); + bool loadV3(QJsonObject &root); + bool saveList(); + + MinecraftAccountPtr defaultAccount() const; + void setDefaultAccount(MinecraftAccountPtr profileId); + bool anyAccountIsValid(); + + bool isActive() const; + +protected: + void beginActivity(); + void endActivity(); + +private: + const char* m_name; + uint32_t m_activityCount = 0; +signals: + void listChanged(); + void listActivityChanged(); + void defaultAccountChanged(); + void activityChanged(bool active); + +public slots: + /** + * This is called when one of the accounts changes and the list needs to be updated + */ + void accountChanged(); + + /** + * This is called when a (refresh/login) task involving the account starts or ends + */ + void accountActivityChanged(bool active); + + /** + * This is initially to run background account refresh tasks, or on a hourly timer + */ + void fillQueue(); + +private slots: + void tryNext(); + + void authSucceeded(); + void authFailed(QString reason); + +protected: + QList m_refreshQueue; + QTimer *m_refreshTimer; + QTimer *m_nextTimer; + shared_qobject_ptr m_currentTask; + + /*! + * Called whenever the list changes. + * This emits the listChanged() signal and autosaves the list (if autosave is enabled). + */ + void onListChanged(); + + /*! + * Called whenever the active account changes. + * Emits the defaultAccountChanged() signal and autosaves the list if enabled. + */ + void onDefaultAccountChanged(); + + QList m_accounts; + + MinecraftAccountPtr m_defaultAccount; + + //! Path to the account list file. Empty string if there isn't one. + QString m_listFilePath; + + /*! + * If true, the account list will automatically save to the account list path when it changes. + * Ignored if m_listFilePath is blank. + */ + bool m_autosave = false; +}; diff --git a/ultimmc/launcher/minecraft/auth/AccountTask.cpp b/ultimmc/launcher/minecraft/auth/AccountTask.cpp new file mode 100644 index 0000000..e6f1cd3 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/AccountTask.cpp @@ -0,0 +1,114 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AccountTask.h" +#include "MinecraftAccount.h" + +#include +#include +#include +#include +#include +#include + +#include + +AccountTask::AccountTask(AccountData *data, QObject *parent) + : Task(parent), m_data(data) +{ + changeState(AccountTaskState::STATE_CREATED); +} + +QString AccountTask::getStateMessage() const +{ + switch (m_taskState) + { + case AccountTaskState::STATE_CREATED: + return "Waiting..."; + case AccountTaskState::STATE_WORKING: + return tr("Sending request to auth servers..."); + case AccountTaskState::STATE_SUCCEEDED: + return tr("Authentication task succeeded."); + case AccountTaskState::STATE_OFFLINE: + return tr("Failed to contact the authentication server."); + case AccountTaskState::STATE_FAILED_SOFT: + return tr("Encountered an error during authentication."); + case AccountTaskState::STATE_FAILED_MUST_MIGRATE: + return tr("Failed to authenticate. The account must be migrated to a Microsoft account to be usable."); + case AccountTaskState::STATE_FAILED_HARD: + return tr("Failed to authenticate. The session has expired."); + case AccountTaskState::STATE_FAILED_GONE: + return tr("Failed to authenticate. The account no longer exists."); + default: + return tr("..."); + } +} + +bool AccountTask::changeState(AccountTaskState newState, QString reason) +{ + m_taskState = newState; + setStatus(getStateMessage()); + switch(newState) { + case AccountTaskState::STATE_CREATED: { + m_data->errorString.clear(); + return true; + } + case AccountTaskState::STATE_WORKING: { + m_data->accountState = AccountState::Working; + return true; + } + case AccountTaskState::STATE_SUCCEEDED: { + m_data->accountState = AccountState::Online; + emitSucceeded(); + return false; + } + case AccountTaskState::STATE_OFFLINE: { + m_data->errorString = reason; + m_data->accountState = AccountState::Offline; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_SOFT: { + m_data->errorString = reason; + m_data->accountState = AccountState::Errored; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_MUST_MIGRATE: { + m_data->errorString = reason; + m_data->accountState = AccountState::MustMigrate; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_HARD: { + m_data->errorString = reason; + m_data->accountState = AccountState::Expired; + emitFailed(reason); + return false; + } + case AccountTaskState::STATE_FAILED_GONE: { + m_data->errorString = reason; + m_data->accountState = AccountState::Gone; + emitFailed(reason); + return false; + } + default: { + QString error = tr("Unknown account task state: %1").arg(int(newState)); + m_data->accountState = AccountState::Errored; + emitFailed(error); + return false; + } + } +} diff --git a/ultimmc/launcher/minecraft/auth/AccountTask.h b/ultimmc/launcher/minecraft/auth/AccountTask.h new file mode 100644 index 0000000..3c1a398 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/AccountTask.h @@ -0,0 +1,77 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include +#include + +#include "MinecraftAccount.h" + +class QNetworkReply; + +/** + * Enum for describing the state of the current task. + * Used by the getStateMessage function to determine what the status message should be. + */ +enum class AccountTaskState +{ + STATE_CREATED, + STATE_WORKING, + STATE_SUCCEEDED, + STATE_FAILED_SOFT, //!< soft failure. authentication went through partially + STATE_FAILED_MUST_MIGRATE, //!< soft failure. main tokens are valid, but the account must be migrated + STATE_FAILED_HARD, //!< hard failure. main tokens are invalid + STATE_FAILED_GONE, //!< hard failure. main tokens are invalid, and the account no longer exists + STATE_OFFLINE //!< soft failure. authentication failed in the first step in a 'soft' way +}; + +class AccountTask : public Task +{ + Q_OBJECT +public: + explicit AccountTask(AccountData * data, QObject *parent = 0); + virtual ~AccountTask() {}; + + AccountTaskState m_taskState = AccountTaskState::STATE_CREATED; + + AccountTaskState taskState() { + return m_taskState; + } + +signals: + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + void hideVerificationUriAndCode(); + +protected: + + /** + * Returns the state message for the given state. + * Used to set the status message for the task. + * Should be overridden by subclasses that want to change messages for a given state. + */ + virtual QString getStateMessage() const; + +protected slots: + // NOTE: true -> non-terminal state, false -> terminal state + bool changeState(AccountTaskState newState, QString reason = QString()); + +protected: + AccountData *m_data = nullptr; +}; diff --git a/ultimmc/launcher/minecraft/auth/AuthProviders.cpp b/ultimmc/launcher/minecraft/auth/AuthProviders.cpp new file mode 100644 index 0000000..e4301d1 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/AuthProviders.cpp @@ -0,0 +1,40 @@ +#include "AuthProviders.h" +#include "providers/ElybyAuthProvider.h" +#include "providers/LocalAuthProvider.h" +#include "providers/MojangAuthProvider.h" +#include "providers/MicrosoftAuthProvider.h" +#include "../../AuthServer.h" + +#define REGISTER_AUTH_PROVIDER(Provider) \ + { \ + AuthProviderPtr provider(new Provider()); \ + m_providers.insert(provider->id(), provider); \ + provider->setAuthServer(authserver); \ + } + +namespace AuthProviders +{ + QMap m_providers; + + void load(std::shared_ptr authserver) + { + REGISTER_AUTH_PROVIDER(ElybyAuthProvider); + REGISTER_AUTH_PROVIDER(LocalAuthProvider); + REGISTER_AUTH_PROVIDER(MojangAuthProvider); + REGISTER_AUTH_PROVIDER(MicrosoftAuthProvider); + } + + AuthProviderPtr lookup(QString id) + { + qDebug() << "LOOKUP AUTH_PROVIDER" << id; + if (m_providers.contains(id)) + return m_providers.value(id); + + qDebug() << "Lookup failed"; + return nullptr; + } + + QList getAll() { + return m_providers.values(); + } +} diff --git a/ultimmc/launcher/minecraft/auth/AuthProviders.h b/ultimmc/launcher/minecraft/auth/AuthProviders.h new file mode 100644 index 0000000..a81c20a --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/AuthProviders.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include "QObjectPtr.h" +#include +#include +#include +#include + +#include "providers/BaseAuthProvider.h" +#include "../../AuthServer.h" + +/*! + * \brief Namespace for auth providers. + * This class main putpose is to handle registration and lookup of auth providers + */ +namespace AuthProviders +{ + void load(std::shared_ptr authServer); + AuthProviderPtr lookup(QString id); + QList getAll(); +} diff --git a/ultimmc/launcher/minecraft/auth/AuthRequest.cpp b/ultimmc/launcher/minecraft/auth/AuthRequest.cpp new file mode 100644 index 0000000..feface8 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/AuthRequest.cpp @@ -0,0 +1,125 @@ +#include + +#include +#include +#include +#include + +#include "Application.h" +#include "AuthRequest.h" +#include "katabasis/Globals.h" + +AuthRequest::AuthRequest(QObject *parent): QObject(parent) { +} + +AuthRequest::~AuthRequest() { +} + +void AuthRequest::get(const QNetworkRequest &req, int timeout/* = 60*1000*/) { + setup(req, QNetworkAccessManager::GetOperation); + reply_ = APPLICATION->network()->get(request_); + status_ = Requesting; + timedReplies_.add(new Katabasis::Reply(reply_, timeout)); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); + connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); +} + +void AuthRequest::post(const QNetworkRequest &req, const QByteArray &data, int timeout/* = 60*1000*/) { + setup(req, QNetworkAccessManager::PostOperation); + data_ = data; + status_ = Requesting; + reply_ = APPLICATION->network()->post(request_, data_); + timedReplies_.add(new Katabasis::Reply(reply_, timeout)); + connect(reply_, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onRequestError(QNetworkReply::NetworkError))); + connect(reply_, SIGNAL(finished()), this, SLOT(onRequestFinished())); + connect(reply_, &QNetworkReply::sslErrors, this, &AuthRequest::onSslErrors); + connect(reply_, SIGNAL(uploadProgress(qint64,qint64)), this, SLOT(onUploadProgress(qint64,qint64))); +} + +void AuthRequest::onRequestFinished() { + if (status_ == Idle) { + return; + } + if (reply_ != qobject_cast(sender())) { + return; + } + httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + finish(); +} + +void AuthRequest::onRequestError(QNetworkReply::NetworkError error) { + qWarning() << "AuthRequest::onRequestError: Error" << (int)error; + if (status_ == Idle) { + return; + } + if (reply_ != qobject_cast(sender())) { + return; + } + errorString_ = reply_->errorString(); + httpStatus_ = reply_->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + error_ = error; + qWarning() << "AuthRequest::onRequestError: Error string: " << errorString_; + qWarning() << "AuthRequest::onRequestError: HTTP status" << httpStatus_ << reply_->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + + // QTimer::singleShot(10, this, SLOT(finish())); +} + +void AuthRequest::onSslErrors(QList errors) { + int i = 1; + for (auto error : errors) { + qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + qCritical() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + +void AuthRequest::onUploadProgress(qint64 uploaded, qint64 total) { + if (status_ == Idle) { + qWarning() << "AuthRequest::onUploadProgress: No pending request"; + return; + } + if (reply_ != qobject_cast(sender())) { + return; + } + // Restart timeout because request in progress + Katabasis::Reply *o2Reply = timedReplies_.find(reply_); + if(o2Reply) { + o2Reply->start(); + } + emit uploadProgress(uploaded, total); +} + +void AuthRequest::setup(const QNetworkRequest &req, QNetworkAccessManager::Operation operation, const QByteArray &verb) { + request_ = req; + operation_ = operation; + url_ = req.url(); + + QUrl url = url_; + request_.setUrl(url); + + if (!verb.isEmpty()) { + request_.setRawHeader(Katabasis::HTTP_HTTP_HEADER, verb); + } + + status_ = Requesting; + error_ = QNetworkReply::NoError; + errorString_.clear(); + httpStatus_ = 0; +} + +void AuthRequest::finish() { + QByteArray data; + if (status_ == Idle) { + qWarning() << "AuthRequest::finish: No pending request"; + return; + } + data = reply_->readAll(); + status_ = Idle; + timedReplies_.remove(reply_); + reply_->disconnect(this); + reply_->deleteLater(); + QList headers = reply_->rawHeaderPairs(); + emit finished(error_, data, headers); +} diff --git a/ultimmc/launcher/minecraft/auth/AuthRequest.h b/ultimmc/launcher/minecraft/auth/AuthRequest.h new file mode 100644 index 0000000..89f7a12 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/AuthRequest.h @@ -0,0 +1,70 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +#include "katabasis/Reply.h" + +/// Makes authentication requests. +class AuthRequest: public QObject { + Q_OBJECT + +public: + explicit AuthRequest(QObject *parent = 0); + ~AuthRequest(); + +public slots: + void get(const QNetworkRequest &req, int timeout = 60*1000); + void post(const QNetworkRequest &req, const QByteArray &data, int timeout = 60*1000); + + +signals: + + /// Emitted when a request has been completed or failed. + void finished(QNetworkReply::NetworkError error, QByteArray data, QList headers); + + /// Emitted when an upload has progressed. + void uploadProgress(qint64 bytesSent, qint64 bytesTotal); + +protected slots: + + /// Handle request finished. + void onRequestFinished(); + + /// Handle request error. + void onRequestError(QNetworkReply::NetworkError error); + + /// Handle ssl errors. + void onSslErrors(QList errors); + + /// Finish the request, emit finished() signal. + void finish(); + + /// Handle upload progress. + void onUploadProgress(qint64 uploaded, qint64 total); + +public: + QNetworkReply::NetworkError error_; + int httpStatus_ = 0; + QString errorString_; + +protected: + void setup(const QNetworkRequest &request, QNetworkAccessManager::Operation operation, const QByteArray &verb = QByteArray()); + + enum Status { + Idle, Requesting, ReRequesting + }; + + QNetworkRequest request_; + QByteArray data_; + QNetworkReply *reply_; + Status status_; + QNetworkAccessManager::Operation operation_; + QUrl url_; + Katabasis::ReplyList timedReplies_; + + QTimer *timer_; +}; diff --git a/ultimmc/launcher/minecraft/auth/AuthSession.cpp b/ultimmc/launcher/minecraft/auth/AuthSession.cpp new file mode 100644 index 0000000..6bea74a --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/AuthSession.cpp @@ -0,0 +1,37 @@ +#include "AuthSession.h" +#include +#include +#include +#include + +QString AuthSession::serializeUserProperties() +{ + QJsonObject userAttrs; + /* + for (auto key : u.properties.keys()) + { + auto array = QJsonArray::fromStringList(u.properties.values(key)); + userAttrs.insert(key, array); + } + */ + QJsonDocument value(userAttrs); + return value.toJson(QJsonDocument::Compact); + +} + +bool AuthSession::MakeOffline(QString offline_playername) +{ + if (status != PlayableOffline && status != PlayableOnline) + { + return false; + } + session = "-"; + player_name = offline_playername; + status = PlayableOffline; + return true; +} + +void AuthSession::MakeDemo() { + player_name = "Player"; + demo = true; +} diff --git a/ultimmc/launcher/minecraft/auth/AuthSession.h b/ultimmc/launcher/minecraft/auth/AuthSession.h new file mode 100644 index 0000000..a75df50 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/AuthSession.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include +#include "QObjectPtr.h" + +class MinecraftAccount; +class QNetworkAccessManager; + +struct AuthSession +{ + bool MakeOffline(QString offline_playername); + void MakeDemo(); + + QString serializeUserProperties(); + + enum Status + { + Undetermined, + RequiresOAuth, + RequiresPassword, + RequiresProfileSetup, + PlayableOffline, + PlayableOnline, + GoneOrMigrated + } status = Undetermined; + + // client token + QString client_token; + // account user name + QString username; + // combined session ID + QString session; + // volatile auth token + QString access_token; + // profile name + QString player_name; + // profile ID + QString uuid; + // 'legacy' or 'mojang', depending on account type + QString user_type; + // Did the auth server reply? + bool auth_server_online = false; + // Did the user request online mode? + bool wants_online = true; + + //Is this a demo session? + bool demo = false; +}; + +typedef std::shared_ptr AuthSessionPtr; diff --git a/ultimmc/launcher/minecraft/auth/AuthStep.cpp b/ultimmc/launcher/minecraft/auth/AuthStep.cpp new file mode 100644 index 0000000..ffa2581 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/AuthStep.cpp @@ -0,0 +1,7 @@ +#include "AuthStep.h" + +AuthStep::AuthStep(AccountData *data) : QObject(nullptr), m_data(data) { +} + +AuthStep::~AuthStep() noexcept = default; + diff --git a/ultimmc/launcher/minecraft/auth/AuthStep.h b/ultimmc/launcher/minecraft/auth/AuthStep.h new file mode 100644 index 0000000..2a8dc2c --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/AuthStep.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AccountData.h" +#include "AccountTask.h" + +class AuthStep : public QObject { + Q_OBJECT + +public: + using Ptr = shared_qobject_ptr; + +public: + explicit AuthStep(AccountData *data); + virtual ~AuthStep() noexcept; + + virtual QString describe() = 0; + +public slots: + virtual void perform() = 0; + virtual void rehydrate() = 0; + +signals: + void finished(AccountTaskState resultingState, QString message); + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + void hideVerificationUriAndCode(); + +protected: + AccountData *m_data; +}; diff --git a/ultimmc/launcher/minecraft/auth/MinecraftAccount.cpp b/ultimmc/launcher/minecraft/auth/MinecraftAccount.cpp new file mode 100644 index 0000000..075c9e1 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/MinecraftAccount.cpp @@ -0,0 +1,362 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Authors: Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MinecraftAccount.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "AuthProviders.h" +#include "flows/MSA.h" +#include "flows/Mojang.h" +#include "flows/Local.h" +#include "flows/Elyby.h" + +MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { + data.internalId = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); +} + + +MinecraftAccountPtr MinecraftAccount::loadFromJsonV2(const QJsonObject& json) { + MinecraftAccountPtr account(new MinecraftAccount()); + if(account->data.resumeStateFromV2(json)) { + return account; + } + return nullptr; +} + +MinecraftAccountPtr MinecraftAccount::loadFromJsonV3(const QJsonObject& json) { + MinecraftAccountPtr account(new MinecraftAccount()); + if(account->data.resumeStateFromV3(json)) { + return account; + } + return nullptr; +} + +MinecraftAccountPtr MinecraftAccount::createFromUsername(const QString &username) +{ + MinecraftAccountPtr account = new MinecraftAccount(); + account->data.type = AccountType::Mojang; + account->data.yggdrasilToken.extra["userName"] = username; + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + return account; +} + +// Taken from Prism Launcher, just for compatibility with their UUIDs +static QUuid uuidFromUsername(QString username) +{ + auto input = QString("OfflinePlayer:%1").arg(username).toUtf8(); + + // basically a reimplementation of Java's UUID#nameUUIDFromBytes + QByteArray digest = QCryptographicHash::hash(input, QCryptographicHash::Md5); + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + auto bOr = [](QByteArray& array, int index, char value) { array[index] = array.at(index) | value; }; + auto bAnd = [](QByteArray& array, int index, char value) { array[index] = array.at(index) & value; }; +#else + auto bOr = [](QByteArray& array, qsizetype index, char value) { array[index] |= value; }; + auto bAnd = [](QByteArray& array, qsizetype index, char value) { array[index] &= value; }; +#endif + bAnd(digest, 6, (char)0x0f); // clear version + bOr(digest, 6, (char)0x30); // set to version 3 + bAnd(digest, 8, (char)0x3f); // clear variant + bOr(digest, 8, (char)0x80); // set to IETF variant + + return QUuid::fromRfc4122(digest); +} + +MinecraftAccountPtr MinecraftAccount::createLocal(const QString &username) +{ + MinecraftAccountPtr account = new MinecraftAccount(); + account->data.type = AccountType::Local; + account->data.yggdrasilToken.validity = Katabasis::Validity::Certain; + account->data.yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); + account->data.yggdrasilToken.extra["userName"] = username; + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegExp("[{}-]")); + account->data.minecraftProfile.name = username; + account->data.minecraftProfile.validity = Katabasis::Validity::Certain; + account->data.minecraftEntitlement.ownsMinecraft = true; + account->data.minecraftEntitlement.canPlayMinecraft = true; + return account; +} + +MinecraftAccountPtr MinecraftAccount::createElyby(const QString &username) +{ + MinecraftAccountPtr account = new MinecraftAccount(); + account->data.type = AccountType::Elyby; + account->data.yggdrasilToken.extra["userName"] = username; + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegExp("[{}-]")); + account->data.minecraftProfile.id = uuidFromUsername(username).toString().remove(QRegExp("[{}-]")); + account->data.minecraftProfile.name = username; + account->data.minecraftProfile.validity = Katabasis::Validity::Certain; + account->data.minecraftEntitlement.ownsMinecraft = true; + account->data.minecraftEntitlement.canPlayMinecraft = true; + return account; +} + +MinecraftAccountPtr MinecraftAccount::createBlankMSA() +{ + MinecraftAccountPtr account(new MinecraftAccount()); + account->data.type = AccountType::MSA; + account->setProvider(AuthProviders::lookup("MSA")); + return account; +} + + +QJsonObject MinecraftAccount::saveToJson() const +{ + return data.saveState(); +} + +AccountState MinecraftAccount::accountState() const { + return data.accountState; +} + +QPixmap MinecraftAccount::getFace() const { + QPixmap skinTexture; + if(!skinTexture.loadFromData(data.minecraftProfile.skin.data, "PNG")) { + return QPixmap(); + } + QPixmap skin = QPixmap(8, 8); + QPainter painter(&skin); + painter.drawPixmap(0, 0, skinTexture.copy(8, 8, 8, 8)); + painter.drawPixmap(0, 0, skinTexture.copy(40, 8, 8, 8)); + return skin.scaled(64, 64, Qt::KeepAspectRatio); +} + + +shared_qobject_ptr MinecraftAccount::login(QString password) { + Q_ASSERT(m_currentTask.get() == nullptr); + + m_currentTask.reset(new MojangLogin(&data, password)); + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + emit activityChanged(true); + return m_currentTask; +} + +shared_qobject_ptr MinecraftAccount::loginMSA() { + Q_ASSERT(m_currentTask.get() == nullptr); + + m_currentTask.reset(new MSAInteractive(&data)); + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + emit activityChanged(true); + return m_currentTask; +} + +shared_qobject_ptr MinecraftAccount::loginLocal() { + Q_ASSERT(m_currentTask.get() == nullptr); + + m_currentTask.reset(new LocalLogin(&data)); + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + emit activityChanged(true); + return m_currentTask; +} + +shared_qobject_ptr MinecraftAccount::loginElyby(QString password) { + Q_ASSERT(m_currentTask.get() == nullptr); + + m_currentTask.reset(new ElybyLogin(&data, password)); + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + emit activityChanged(true); + return m_currentTask; +} + +shared_qobject_ptr MinecraftAccount::refresh() { + if(m_currentTask) { + return m_currentTask; + } + + if(data.type == AccountType::MSA) { + m_currentTask.reset(new MSASilent(&data)); + } + else if (data.type == AccountType::Mojang) { + m_currentTask.reset(new MojangRefresh(&data)); + } + else if (data.type == AccountType::Local) { + m_currentTask.reset(new LocalRefresh(&data)); + } + else if (data.type == AccountType::Elyby) { + m_currentTask.reset(new ElybyRefresh(&data)); + } + + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + emit activityChanged(true); + return m_currentTask; +} + +shared_qobject_ptr MinecraftAccount::currentTask() { + return m_currentTask; +} + + +void MinecraftAccount::authSucceeded() +{ + m_currentTask.reset(); + emit changed(); + emit activityChanged(false); +} + +void MinecraftAccount::authFailed(QString reason) +{ + switch (m_currentTask->taskState()) { + case AccountTaskState::STATE_OFFLINE: + case AccountTaskState::STATE_FAILED_MUST_MIGRATE: + case AccountTaskState::STATE_FAILED_SOFT: { + // NOTE: this doesn't do much. There was an error of some sort. + } + break; + case AccountTaskState::STATE_FAILED_HARD: { + if(isMSA()) { + data.msaToken.token = QString(); + data.msaToken.refresh_token = QString(); + data.msaToken.validity = Katabasis::Validity::None; + data.validity_ = Katabasis::Validity::None; + } + else { + data.yggdrasilToken.token = QString(); + data.yggdrasilToken.validity = Katabasis::Validity::None; + data.validity_ = Katabasis::Validity::None; + } + emit changed(); + } + break; + case AccountTaskState::STATE_FAILED_GONE: { + data.validity_ = Katabasis::Validity::None; + emit changed(); + } + break; + case AccountTaskState::STATE_CREATED: + case AccountTaskState::STATE_WORKING: + case AccountTaskState::STATE_SUCCEEDED: { + // Not reachable here, as they are not failures. + } + } + m_currentTask.reset(); + emit activityChanged(false); +} + +bool MinecraftAccount::isActive() const { + return m_currentTask; +} + +bool MinecraftAccount::shouldRefresh() const { + /* + * Never refresh accounts that are being used by the game, it breaks the game session. + * Always refresh accounts that have not been refreshed yet during this session. + * Don't refresh broken accounts. + * Refresh accounts that would expire in the next 12 hours (fresh token validity is 24 hours). + */ + if(isInUse()) { + return false; + } + switch(data.validity_) { + case Katabasis::Validity::Certain: { + break; + } + case Katabasis::Validity::None: { + return false; + } + case Katabasis::Validity::Assumed: { + return true; + } + } + auto now = QDateTime::currentDateTimeUtc(); + auto issuedTimestamp = data.yggdrasilToken.issueInstant; + auto expiresTimestamp = data.yggdrasilToken.notAfter; + + if(!expiresTimestamp.isValid()) { + expiresTimestamp = issuedTimestamp.addSecs(24 * 3600); + } + if (now.secsTo(expiresTimestamp) < (12 * 3600)) { + return true; + } + return false; +} + +void MinecraftAccount::fillSession(AuthSessionPtr session) +{ + if(ownsMinecraft() && !hasProfile()) { + session->status = AuthSession::RequiresProfileSetup; + } + else { + if(session->wants_online) { + session->status = AuthSession::PlayableOnline; + } + else { + session->status = AuthSession::PlayableOffline; + } + } + + // the user name. you have to have an user name + // FIXME: not with MSA + session->username = data.userName(); + // volatile auth token + session->access_token = data.accessToken(); + // the semi-permanent client token + session->client_token = data.clientToken(); + // profile name + session->player_name = data.profileName(); + // profile ID + session->uuid = data.profileId(); + // 'legacy' or 'mojang', depending on account type + session->user_type = typeString(); + if (!session->access_token.isEmpty()) + { + session->session = "token:" + data.accessToken() + ":" + data.profileId(); + } + else + { + session->session = "-"; + } +} + +void MinecraftAccount::decrementUses() +{ + Usable::decrementUses(); + if(!isInUse()) + { + emit changed(); + // FIXME: we now need a better way to identify accounts... + qWarning() << "Profile" << data.profileId() << "is no longer in use."; + } +} + +void MinecraftAccount::incrementUses() +{ + bool wasInUse = isInUse(); + Usable::incrementUses(); + if(!wasInUse) + { + emit changed(); + // FIXME: we now need a better way to identify accounts... + qWarning() << "Profile" << data.profileId() << "is now in use."; + } +} diff --git a/ultimmc/launcher/minecraft/auth/MinecraftAccount.h b/ultimmc/launcher/minecraft/auth/MinecraftAccount.h new file mode 100644 index 0000000..a82f14e --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/MinecraftAccount.h @@ -0,0 +1,228 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "AuthSession.h" +#include "Usable.h" +#include "AccountData.h" +#include "QObjectPtr.h" + +#include "providers/BaseAuthProvider.h" + +class Task; +class AccountTask; +class MinecraftAccount; + +typedef shared_qobject_ptr MinecraftAccountPtr; +Q_DECLARE_METATYPE(MinecraftAccountPtr) + +/** + * A profile within someone's Mojang account. + * + * Currently, the profile system has not been implemented by Mojang yet, + * but we might as well add some things for it in MultiMC right now so + * we don't have to rip the code to pieces to add it later. + */ +// Defined in providers/BaseAuthProvider.h +//struct AccountProfile +//{ + //QString id; + //QString name; + //bool legacy; +//}; + +/** + * Object that stores information about a certain Mojang account. + * + * Said information may include things such as that account's username, client token, and access + * token if the user chose to stay logged in. + */ +class MinecraftAccount : + public QObject, + public Usable +{ + Q_OBJECT +public: /* construction */ + //! Do not copy accounts. ever. + explicit MinecraftAccount(const MinecraftAccount &other, QObject *parent) = delete; + + //! Default constructor + explicit MinecraftAccount(QObject *parent = 0); + + static MinecraftAccountPtr createFromUsername(const QString &username); + + static MinecraftAccountPtr createLocal(const QString &username); + + static MinecraftAccountPtr createElyby(const QString &username); + + static MinecraftAccountPtr createBlankMSA(); + + static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json); + static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json); + + //! Saves a MinecraftAccount to a JSON object and returns it. + QJsonObject saveToJson() const; + +public: /* manipulation */ + + /** + * Attempt to login. Empty password means we use the token. + * If the attempt fails because we already are performing some task, it returns false. + */ + shared_qobject_ptr login(QString password); + + shared_qobject_ptr loginMSA(); + + shared_qobject_ptr loginLocal(); + + shared_qobject_ptr loginElyby(QString password); + + shared_qobject_ptr refresh(); + + shared_qobject_ptr currentTask(); + +public: /* queries */ + bool setProvider(AuthProviderPtr provider) { + data.provider = provider; + return true; + } + + AuthProviderPtr provider() { + return data.provider; + } + + QString internalId() const { + return data.internalId; + } + + QString accountDisplayString() const { + return data.accountDisplayString(); + } + + QString mojangUserName() const { + return data.userName(); + } + + QString accessToken() const { + return data.accessToken(); + } + + QString profileId() const { + return data.profileId(); + } + + QString profileName() const { + return data.profileName(); + } + + bool isActive() const; + + bool canMigrate() const { + return data.canMigrateToMSA; + } + + bool isMSA() const { + return data.type == AccountType::MSA; + } + + bool ownsMinecraft() const { + return data.minecraftEntitlement.ownsMinecraft; + } + + bool hasProfile() const { + return data.profileId().size() != 0; + } + + QString typeString() const { + switch(data.type) { + case AccountType::Mojang: { + if(data.legacy) { + return "legacy"; + } + return data.provider->displayName(); + } + break; + case AccountType::MSA: { + return "msa"; + } + break; + case AccountType::Local: { + return "local"; + } + break; + case AccountType::Elyby: { + return "elyby"; + } + break; + default: { + return "unknown"; + } + } + } + + QPixmap getFace() const; + + //! Returns the current state of the account + AccountState accountState() const; + + AccountData * accountData() { + return &data; + } + + bool shouldRefresh() const; + + void fillSession(AuthSessionPtr session); + + QString lastError() const { + return data.lastError(); + } + +signals: + /** + * This signal is emitted when the account changes + */ + void changed(); + + void activityChanged(bool active); + + // TODO: better signalling for the various possible state changes - especially errors + +protected: /* variables */ + AccountData data; + + // current task we are executing here + shared_qobject_ptr m_currentTask; + +protected: /* methods */ + + void incrementUses() override; + void decrementUses() override; + +private +slots: + void authSucceeded(); + void authFailed(QString reason); +}; diff --git a/ultimmc/launcher/minecraft/auth/Parsers.cpp b/ultimmc/launcher/minecraft/auth/Parsers.cpp new file mode 100644 index 0000000..c2f6478 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/Parsers.cpp @@ -0,0 +1,348 @@ +#include "Parsers.h" + +#include +#include +#include + +namespace Parsers { + +bool getDateTime(QJsonValue value, QDateTime & out) { + if(!value.isString()) { + return false; + } + out = QDateTime::fromString(value.toString(), Qt::ISODate); + return out.isValid(); +} + +bool getString(QJsonValue value, QString & out) { + if(!value.isString()) { + return false; + } + out = value.toString(); + return true; +} + +bool getNumber(QJsonValue value, double & out) { + if(!value.isDouble()) { + return false; + } + out = value.toDouble(); + return true; +} + +bool getNumber(QJsonValue value, int64_t & out) { + if(!value.isDouble()) { + return false; + } + out = (int64_t) value.toDouble(); + return true; +} + +bool getBool(QJsonValue value, bool & out) { + if(!value.isBool()) { + return false; + } + out = value.toBool(); + return true; +} + +/* +{ + "IssueInstant":"2020-12-07T19:52:08.4463796Z", + "NotAfter":"2020-12-21T19:52:08.4463796Z", + "Token":"token", + "DisplayClaims":{ + "xui":[ + { + "uhs":"userhash" + } + ] + } + } +*/ +// TODO: handle error responses ... +/* +{ + "Identity":"0", + "XErr":2148916238, + "Message":"", + "Redirect":"https://start.ui.xboxlive.com/AddChildToFamily" +} +// 2148916233 = missing XBox account +// 2148916238 = child account not linked to a family +*/ + +bool parseXTokenResponse(QByteArray & data, Katabasis::Token &output, QString name) { + qDebug() << "Parsing" << name <<":"; +#ifndef NDEBUG + qDebug() << data; +#endif + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + if(!getDateTime(obj.value("IssueInstant"), output.issueInstant)) { + qWarning() << "User IssueInstant is not a timestamp"; + return false; + } + if(!getDateTime(obj.value("NotAfter"), output.notAfter)) { + qWarning() << "User NotAfter is not a timestamp"; + return false; + } + if(!getString(obj.value("Token"), output.token)) { + qWarning() << "User Token is not a string"; + return false; + } + auto arrayVal = obj.value("DisplayClaims").toObject().value("xui"); + if(!arrayVal.isArray()) { + qWarning() << "Missing xui claims array"; + return false; + } + bool foundUHS = false; + for(auto item: arrayVal.toArray()) { + if(!item.isObject()) { + continue; + } + auto obj = item.toObject(); + if(obj.contains("uhs")) { + foundUHS = true; + } else { + continue; + } + // consume all 'display claims' ... whatever that means + for(auto iter = obj.begin(); iter != obj.end(); iter++) { + QString claim; + if(!getString(obj.value(iter.key()), claim)) { + qWarning() << "display claim " << iter.key() << " is not a string..."; + return false; + } + output.extra[iter.key()] = claim; + } + + break; + } + if(!foundUHS) { + qWarning() << "Missing uhs"; + return false; + } + output.validity = Katabasis::Validity::Certain; + qDebug() << name << "is valid."; + return true; +} + +bool parseMinecraftProfile(QByteArray & data, MinecraftProfile &output) { + qDebug() << "Parsing Minecraft profile..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + if(!getString(obj.value("id"), output.id)) { + qWarning() << "Minecraft profile id is not a string"; + return false; + } + + if(!getString(obj.value("name"), output.name)) { + qWarning() << "Minecraft profile name is not a string"; + return false; + } + + auto skinsArray = obj.value("skins").toArray(); + for(auto skin: skinsArray) { + auto skinObj = skin.toObject(); + Skin skinOut; + if(!getString(skinObj.value("id"), skinOut.id)) { + continue; + } + QString state; + if(!getString(skinObj.value("state"), state)) { + continue; + } + if(state != "ACTIVE") { + continue; + } + if(!getString(skinObj.value("url"), skinOut.url)) { + continue; + } + if(!getString(skinObj.value("variant"), skinOut.variant)) { + continue; + } + // we deal with only the active skin + output.skin = skinOut; + break; + } + auto capesArray = obj.value("capes").toArray(); + + QString currentCape; + for(auto cape: capesArray) { + auto capeObj = cape.toObject(); + Cape capeOut; + if(!getString(capeObj.value("id"), capeOut.id)) { + continue; + } + QString state; + if(!getString(capeObj.value("state"), state)) { + continue; + } + if(state == "ACTIVE") { + currentCape = capeOut.id; + } + if(!getString(capeObj.value("url"), capeOut.url)) { + continue; + } + if(!getString(capeObj.value("alias"), capeOut.alias)) { + continue; + } + + output.capes[capeOut.id] = capeOut; + } + output.currentCape = currentCape; + output.validity = Katabasis::Validity::Certain; + return true; +} + +bool parseMinecraftEntitlements(QByteArray & data, MinecraftEntitlement &output) { + qDebug() << "Parsing Minecraft entitlements..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from user.auth.xboxlive.com as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + output.canPlayMinecraft = false; + output.ownsMinecraft = false; + + auto itemsArray = obj.value("items").toArray(); + for(auto item: itemsArray) { + auto itemObj = item.toObject(); + QString name; + if(!getString(itemObj.value("name"), name)) { + continue; + } + if(name == "game_minecraft") { + output.canPlayMinecraft = true; + } + if(name == "product_minecraft") { + output.ownsMinecraft = true; + } + } + output.validity = Katabasis::Validity::Certain; + return true; +} + +bool parseForcedMigrationResponse(QByteArray & data, bool& result) { + qDebug() << "Parsing Rollout response..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigrationforced as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + QString feature; + if(!getString(obj.value("feature"), feature)) { + qWarning() << "Rollout feature is not a string"; + return false; + } + if(feature != "msamigrationforced") { + qWarning() << "Rollout feature is not what we expected (msamigrationforced), but is instead \"" << feature << "\""; + return false; + } + if(!getBool(obj.value("rollout"), result)) { + qWarning() << "Rollout feature is not a string"; + return false; + } + return true; +} + +bool parseRolloutResponse(QByteArray & data, bool& result) { + qDebug() << "Parsing Rollout response..."; +#ifndef NDEBUG + qDebug() << data; +#endif + + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from https://api.minecraftservices.com/rollout/v1/msamigration as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + QString feature; + if(!getString(obj.value("feature"), feature)) { + qWarning() << "Rollout feature is not a string"; + return false; + } + if(feature != "msamigration") { + qWarning() << "Rollout feature is not what we expected (msamigration), but is instead \"" << feature << "\""; + return false; + } + if(!getBool(obj.value("rollout"), result)) { + qWarning() << "Rollout feature is not a string"; + return false; + } + return true; +} + +bool parseMojangResponse(QByteArray & data, Katabasis::Token &output) { + QJsonParseError jsonError; + qDebug() << "Parsing Mojang response..."; +#ifndef NDEBUG + qDebug() << data; +#endif + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Failed to parse response from api.minecraftservices.com/launcher/login as JSON: " << jsonError.errorString(); + return false; + } + + auto obj = doc.object(); + double expires_in = 0; + if(!getNumber(obj.value("expires_in"), expires_in)) { + qWarning() << "expires_in is not a valid number"; + return false; + } + auto currentTime = QDateTime::currentDateTimeUtc(); + output.issueInstant = currentTime; + output.notAfter = currentTime.addSecs(expires_in); + + QString username; + if(!getString(obj.value("username"), username)) { + qWarning() << "username is not valid"; + return false; + } + + // TODO: it's a JWT... validate it? + if(!getString(obj.value("access_token"), output.token)) { + qWarning() << "access_token is not valid"; + return false; + } + output.validity = Katabasis::Validity::Certain; + qDebug() << "Mojang response is valid."; + return true; +} + +} diff --git a/ultimmc/launcher/minecraft/auth/Parsers.h b/ultimmc/launcher/minecraft/auth/Parsers.h new file mode 100644 index 0000000..db26271 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/Parsers.h @@ -0,0 +1,20 @@ +#pragma once + +#include "AccountData.h" + +namespace Parsers +{ + bool getDateTime(QJsonValue value, QDateTime & out); + bool getString(QJsonValue value, QString & out); + bool getNumber(QJsonValue value, double & out); + bool getNumber(QJsonValue value, int64_t & out); + bool getBool(QJsonValue value, bool & out); + + bool parseXTokenResponse(QByteArray &data, Katabasis::Token &output, QString name); + bool parseMojangResponse(QByteArray &data, Katabasis::Token &output); + + bool parseMinecraftProfile(QByteArray &data, MinecraftProfile &output); + bool parseMinecraftEntitlements(QByteArray &data, MinecraftEntitlement &output); + bool parseRolloutResponse(QByteArray &data, bool& result); + bool parseForcedMigrationResponse(QByteArray & data, bool& result); +} diff --git a/ultimmc/launcher/minecraft/auth/Yggdrasil.cpp b/ultimmc/launcher/minecraft/auth/Yggdrasil.cpp new file mode 100644 index 0000000..9c1ac5b --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/Yggdrasil.cpp @@ -0,0 +1,331 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Yggdrasil.h" +#include "AccountData.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include "Application.h" + +Yggdrasil::Yggdrasil(AccountData *data, QObject *parent) + : AccountTask(data, parent) +{ + changeState(AccountTaskState::STATE_CREATED); +} + +void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) { + changeState(AccountTaskState::STATE_WORKING); + + QNetworkRequest netRequest(endpoint); + netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + m_netReply = APPLICATION->network()->post(netRequest, content); + connect(m_netReply, &QNetworkReply::finished, this, &Yggdrasil::processReply); + connect(m_netReply, &QNetworkReply::uploadProgress, this, &Yggdrasil::refreshTimers); + connect(m_netReply, &QNetworkReply::downloadProgress, this, &Yggdrasil::refreshTimers); + connect(m_netReply, &QNetworkReply::sslErrors, this, &Yggdrasil::sslErrors); + timeout_keeper.setSingleShot(true); + timeout_keeper.start(timeout_max); + counter.setSingleShot(false); + counter.start(time_step); + progress(0, timeout_max); + connect(&timeout_keeper, &QTimer::timeout, this, &Yggdrasil::abortByTimeout); + connect(&counter, &QTimer::timeout, this, &Yggdrasil::heartbeat); +} + +void Yggdrasil::executeTask() { +} + +void Yggdrasil::refresh() { + start(); + /* + * { + * "clientToken": "client identifier" + * "accessToken": "current access token to be refreshed" + * "selectedProfile": // specifying this causes errors + * { + * "id": "profile ID" + * "name": "profile name" + * } + * "requestUser": true/false // request the user structure + * } + */ + QJsonObject req; + req.insert("clientToken", m_data->clientToken()); + req.insert("accessToken", m_data->accessToken()); + /* + { + auto currentProfile = m_account->currentProfile(); + QJsonObject profile; + profile.insert("id", currentProfile->id()); + profile.insert("name", currentProfile->name()); + req.insert("selectedProfile", profile); + } + */ + req.insert("requestUser", false); + QJsonDocument doc(req); + + QUrl reqUrl(m_data->provider->authEndpoint() + "refresh"); + QByteArray requestData = doc.toJson(); + + sendRequest(reqUrl, requestData); +} + +void Yggdrasil::login(QString password) { + start(); + /* + * { + * "agent": { // optional + * "name": "Minecraft", // So far this is the only encountered value + * "version": 1 // This number might be increased + * // by the vanilla client in the future + * }, + * "username": "mojang account name", // Can be an email address or player name for + * // unmigrated accounts + * "password": "mojang account password", + * "clientToken": "client identifier", // optional + * "requestUser": true/false // request the user structure + * } + */ + QJsonObject req; + + { + QJsonObject agent; + // C++ makes string literals void* for some stupid reason, so we have to tell it + // QString... Thanks Obama. + agent.insert("name", QString("Minecraft")); + agent.insert("version", 1); + req.insert("agent", agent); + } + + req.insert("username", m_data->userName()); + req.insert("password", password); + req.insert("requestUser", false); + + // If we already have a client token, give it to the server. + // Otherwise, let the server give us one. + + m_data->generateClientTokenIfMissing(); + req.insert("clientToken", m_data->clientToken()); + + QJsonDocument doc(req); + + QUrl reqUrl(m_data->provider->authEndpoint() + "authenticate"); + QNetworkRequest netRequest(reqUrl); + QByteArray requestData = doc.toJson(); + + sendRequest(reqUrl, requestData); +} + + + +void Yggdrasil::refreshTimers(qint64, qint64) { + timeout_keeper.stop(); + timeout_keeper.start(timeout_max); + progress(count = 0, timeout_max); +} + +void Yggdrasil::heartbeat() { + count += time_step; + progress(count, timeout_max); +} + +bool Yggdrasil::abort() { + progress(timeout_max, timeout_max); + // TODO: actually use this in a meaningful way + m_aborted = Yggdrasil::BY_USER; + m_netReply->abort(); + return true; +} + +void Yggdrasil::abortByTimeout() { + progress(timeout_max, timeout_max); + // TODO: actually use this in a meaningful way + m_aborted = Yggdrasil::BY_TIMEOUT; + m_netReply->abort(); +} + +void Yggdrasil::sslErrors(QList errors) { + int i = 1; + for (auto error : errors) { + qCritical() << "LOGIN SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + qCritical() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + +void Yggdrasil::processResponse(QJsonObject responseData) { + // Read the response data. We need to get the client token, access token, and the selected + // profile. + qDebug() << "Processing authentication response."; + + // qDebug() << responseData; + // If we already have a client token, make sure the one the server gave us matches our + // existing one. + QString clientToken = responseData.value("clientToken").toString(""); + if (clientToken.isEmpty()) { + // Fail if the server gave us an empty client token + changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send a client token.")); + return; + } + if(m_data->clientToken().isEmpty()) { + m_data->setClientToken(clientToken); + } + else if(clientToken != m_data->clientToken()) { + changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server attempted to change the client token. This isn't supported.")); + return; + } + + // Now, we set the access token. + qDebug() << "Getting access token."; + QString accessToken = responseData.value("accessToken").toString(""); + if (accessToken.isEmpty()) { + // Fail if the server didn't give us an access token. + changeState(AccountTaskState::STATE_FAILED_HARD, tr("Authentication server didn't send an access token.")); + return; + } + // Set the access token. + m_data->yggdrasilToken.token = accessToken; + m_data->yggdrasilToken.validity = Katabasis::Validity::Certain; + m_data->yggdrasilToken.issueInstant = QDateTime::currentDateTimeUtc(); + + // We've made it through the minefield of possible errors. Return true to indicate that + // we've succeeded. + qDebug() << "Finished reading authentication response."; + changeState(AccountTaskState::STATE_SUCCEEDED); +} + +void Yggdrasil::processReply() { + changeState(AccountTaskState::STATE_WORKING); + + switch (m_netReply->error()) + { + case QNetworkReply::NoError: + break; + case QNetworkReply::TimeoutError: + changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation timed out.")); + return; + case QNetworkReply::OperationCanceledError: + changeState(AccountTaskState::STATE_FAILED_SOFT, tr("Authentication operation cancelled.")); + return; + case QNetworkReply::SslHandshakeFailedError: + changeState( + AccountTaskState::STATE_FAILED_SOFT, + tr( + "SSL Handshake failed.
There might be a few causes for it:
" + "
    " + "
  • You use Windows and need to update your root certificates, please install any outstanding updates.
  • " + "
  • Some device on your network is interfering with SSL traffic. In that case, " + "you have bigger worries than Minecraft not starting.
  • " + "
  • Possibly something else. Check the log file for details
  • " + "
" + ) + ); + return; + // used for invalid credentials and similar errors. Fall through. + case QNetworkReply::ContentAccessDenied: + case QNetworkReply::ContentOperationNotPermittedError: + break; + case QNetworkReply::ContentGoneError: { + changeState( + AccountTaskState::STATE_FAILED_GONE, + tr("The Mojang account no longer exists. It may have been migrated to a Microsoft account.") + ); + } + default: + changeState( + AccountTaskState::STATE_FAILED_SOFT, + tr("Authentication operation failed due to a network error: %1 (%2)").arg(m_netReply->errorString()).arg(m_netReply->error()) + ); + return; + } + + // Try to parse the response regardless of the response code. + // Sometimes the auth server will give more information and an error code. + QJsonParseError jsonError; + QByteArray replyData = m_netReply->readAll(); + QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError); + // Check the response code. + int responseCode = m_netReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (responseCode == 200) { + // If the response code was 200, then there shouldn't be an error. Make sure + // anyways. + // Also, sometimes an empty reply indicates success. If there was no data received, + // pass an empty json object to the processResponse function. + if (jsonError.error == QJsonParseError::NoError || replyData.size() == 0) { + processResponse(replyData.size() > 0 ? doc.object() : QJsonObject()); + return; + } + else { + changeState( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to parse authentication server response JSON response: %1 at offset %2.").arg(jsonError.errorString()).arg(jsonError.offset) + ); + qCritical() << replyData; + } + return; + } + + // If the response code was not 200, then Yggdrasil may have given us information + // about the error. + // If we can parse the response, then get information from it. Otherwise just say + // there was an unknown error. + if (jsonError.error == QJsonParseError::NoError) { + // We were able to parse the server's response. Woo! + // Call processError. If a subclass has overridden it then they'll handle their + // stuff there. + qDebug() << "The request failed, but the server gave us an error message. Processing error."; + processError(doc.object()); + } + else { + // The server didn't say anything regarding the error. Give the user an unknown + // error. + qDebug() << "The request failed and the server gave no error message. Unknown error."; + changeState( + AccountTaskState::STATE_FAILED_SOFT, + tr("An unknown error occurred when trying to communicate with the authentication server: %1").arg(m_netReply->errorString()) + ); + } +} + +void Yggdrasil::processError(QJsonObject responseData) { + QJsonValue errorVal = responseData.value("error"); + QJsonValue errorMessageValue = responseData.value("errorMessage"); + QJsonValue causeVal = responseData.value("cause"); + + if (errorVal.isString() && errorMessageValue.isString()) { + m_error = std::shared_ptr( + new Error { + errorVal.toString(""), + errorMessageValue.toString(""), + causeVal.toString("") + } + ); + changeState(AccountTaskState::STATE_FAILED_HARD, m_error->m_errorMessageVerbose); + } + else { + // Error is not in standard format. Don't set m_error and return unknown error. + changeState(AccountTaskState::STATE_FAILED_HARD, tr("An unknown Yggdrasil error occurred.")); + } +} diff --git a/ultimmc/launcher/minecraft/auth/Yggdrasil.h b/ultimmc/launcher/minecraft/auth/Yggdrasil.h new file mode 100644 index 0000000..4f52a04 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/Yggdrasil.h @@ -0,0 +1,102 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "AccountTask.h" + +#include +#include +#include +#include + +#include "MinecraftAccount.h" + +class QNetworkAccessManager; +class QNetworkReply; + +/** + * A Yggdrasil task is a task that performs an operation on a given mojang account. + */ +class Yggdrasil : public AccountTask +{ + Q_OBJECT +public: + explicit Yggdrasil( + AccountData *data, + QObject *parent = 0 + ); + virtual ~Yggdrasil() = default; + + void refresh(); + void login(QString password); + + struct Error + { + QString m_errorMessageShort; + QString m_errorMessageVerbose; + QString m_cause; + }; + std::shared_ptr m_error; + + enum AbortedBy + { + BY_NOTHING, + BY_USER, + BY_TIMEOUT + } m_aborted = BY_NOTHING; + +protected: + void executeTask() override; + + /** + * Processes the response received from the server. + * If an error occurred, this should emit a failed signal. + * If Yggdrasil gave an error response, it should call setError() first, and then return false. + * Otherwise, it should return true. + * Note: If the response from the server was blank, and the HTTP code was 200, this function is called with + * an empty QJsonObject. + */ + void processResponse(QJsonObject responseData); + + /** + * Processes an error response received from the server. + * The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error. + * \returns a QString error message that will be passed to emitFailed. + */ + virtual void processError(QJsonObject responseData); + +protected slots: + void processReply(); + void refreshTimers(qint64, qint64); + void heartbeat(); + void sslErrors(QList); + void abortByTimeout(); + +public slots: + virtual bool abort() override; + +private: + void sendRequest(QUrl endpoint, QByteArray content); + +protected: + QNetworkReply *m_netReply = nullptr; + QTimer timeout_keeper; + QTimer counter; + int count = 0; // num msec since time reset + + const int timeout_max = 30000; + const int time_step = 50; +}; diff --git a/ultimmc/launcher/minecraft/auth/flows/AuthFlow.cpp b/ultimmc/launcher/minecraft/auth/flows/AuthFlow.cpp new file mode 100644 index 0000000..4f78e8c --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/flows/AuthFlow.cpp @@ -0,0 +1,71 @@ +#include +#include +#include +#include + +#include "AuthFlow.h" +#include "katabasis/Globals.h" + +#include + +AuthFlow::AuthFlow(AccountData * data, QObject *parent) : + AccountTask(data, parent) +{ +} + +void AuthFlow::succeed() { + m_data->validity_ = Katabasis::Validity::Certain; + changeState( + AccountTaskState::STATE_SUCCEEDED, + tr("Finished all authentication steps") + ); +} + +void AuthFlow::executeTask() { + if(m_currentStep) { + return; + } + changeState(AccountTaskState::STATE_WORKING, tr("Initializing")); + nextStep(); +} + +void AuthFlow::nextStep() { + if(m_steps.size() == 0) { + // we got to the end without an incident... assume this is all. + m_currentStep.reset(); + succeed(); + return; + } + m_currentStep = m_steps.front(); + qDebug() << "AuthFlow:" << m_currentStep->describe(); + m_steps.pop_front(); + connect(m_currentStep.get(), &AuthStep::finished, this, &AuthFlow::stepFinished); + connect(m_currentStep.get(), &AuthStep::showVerificationUriAndCode, this, &AuthFlow::showVerificationUriAndCode); + connect(m_currentStep.get(), &AuthStep::hideVerificationUriAndCode, this, &AuthFlow::hideVerificationUriAndCode); + + m_currentStep->perform(); +} + + +QString AuthFlow::getStateMessage() const { + switch (m_taskState) + { + case AccountTaskState::STATE_WORKING: { + if(m_currentStep) { + return m_currentStep->describe(); + } + else { + return tr("Working..."); + } + } + default: { + return AccountTask::getStateMessage(); + } + } +} + +void AuthFlow::stepFinished(AccountTaskState resultingState, QString message) { + if(changeState(resultingState, message)) { + nextStep(); + } +} diff --git a/ultimmc/launcher/minecraft/auth/flows/AuthFlow.h b/ultimmc/launcher/minecraft/auth/flows/AuthFlow.h new file mode 100644 index 0000000..e067cc9 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/flows/AuthFlow.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include "minecraft/auth/Yggdrasil.h" +#include "minecraft/auth/AccountData.h" +#include "minecraft/auth/AccountTask.h" +#include "minecraft/auth/AuthStep.h" + +class AuthFlow : public AccountTask +{ + Q_OBJECT + +public: + explicit AuthFlow(AccountData * data, QObject *parent = 0); + + Katabasis::Validity validity() { + return m_data->validity_; + }; + + QString getStateMessage() const override; + + void executeTask() override; + +signals: + void activityChanged(Katabasis::Activity activity); + +private slots: + void stepFinished(AccountTaskState resultingState, QString message); + +protected: + void succeed(); + void nextStep(); + +protected: + QList m_steps; + AuthStep::Ptr m_currentStep; +}; diff --git a/ultimmc/launcher/minecraft/auth/flows/Elyby.cpp b/ultimmc/launcher/minecraft/auth/flows/Elyby.cpp new file mode 100644 index 0000000..e842254 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/flows/Elyby.cpp @@ -0,0 +1,21 @@ +#include "Elyby.h" + +#include "minecraft/auth/steps/YggdrasilStep.h" +#include "minecraft/auth/steps/ElybyProfileStep.h" + +ElybyRefresh::ElybyRefresh( + AccountData *data, + QObject *parent +) : AuthFlow(data, parent) { + m_steps.append(new YggdrasilStep(m_data, QString())); + m_steps.append(new ElybyProfileStep(m_data)); +} + +ElybyLogin::ElybyLogin( + AccountData *data, + QString password, + QObject *parent +): AuthFlow(data, parent), m_password(password) { + m_steps.append(new YggdrasilStep(m_data, m_password)); + m_steps.append(new ElybyProfileStep(m_data)); +} diff --git a/ultimmc/launcher/minecraft/auth/flows/Elyby.h b/ultimmc/launcher/minecraft/auth/flows/Elyby.h new file mode 100644 index 0000000..beec3e6 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/flows/Elyby.h @@ -0,0 +1,26 @@ +#pragma once +#include "AuthFlow.h" + +class ElybyRefresh : public AuthFlow +{ + Q_OBJECT +public: + explicit ElybyRefresh( + AccountData *data, + QObject *parent = 0 + ); +}; + +class ElybyLogin : public AuthFlow +{ + Q_OBJECT +public: + explicit ElybyLogin( + AccountData *data, + QString password, + QObject *parent = 0 + ); + +private: + QString m_password; +}; diff --git a/ultimmc/launcher/minecraft/auth/flows/Local.cpp b/ultimmc/launcher/minecraft/auth/flows/Local.cpp new file mode 100644 index 0000000..d322cb5 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/flows/Local.cpp @@ -0,0 +1,17 @@ +#include "Local.h" + +#include "minecraft/auth/steps/LocalStep.h" + +LocalRefresh::LocalRefresh( + AccountData *data, + QObject *parent +) : AuthFlow(data, parent) { + m_steps.append(new LocalStep(m_data)); +} + +LocalLogin::LocalLogin( + AccountData *data, + QObject *parent +): AuthFlow(data, parent) { + m_steps.append(new LocalStep(m_data)); +} diff --git a/ultimmc/launcher/minecraft/auth/flows/Local.h b/ultimmc/launcher/minecraft/auth/flows/Local.h new file mode 100644 index 0000000..e30de5d --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/flows/Local.h @@ -0,0 +1,22 @@ +#pragma once +#include "AuthFlow.h" + +class LocalRefresh : public AuthFlow +{ + Q_OBJECT +public: + explicit LocalRefresh( + AccountData *data, + QObject *parent = 0 + ); +}; + +class LocalLogin : public AuthFlow +{ + Q_OBJECT +public: + explicit LocalLogin( + AccountData *data, + QObject *parent = 0 + ); +}; diff --git a/ultimmc/launcher/minecraft/auth/flows/MSA.cpp b/ultimmc/launcher/minecraft/auth/flows/MSA.cpp new file mode 100644 index 0000000..416b8f2 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/flows/MSA.cpp @@ -0,0 +1,37 @@ +#include "MSA.h" + +#include "minecraft/auth/steps/MSAStep.h" +#include "minecraft/auth/steps/XboxUserStep.h" +#include "minecraft/auth/steps/XboxAuthorizationStep.h" +#include "minecraft/auth/steps/LauncherLoginStep.h" +#include "minecraft/auth/steps/XboxProfileStep.h" +#include "minecraft/auth/steps/EntitlementsStep.h" +#include "minecraft/auth/steps/MinecraftProfileStep.h" +#include "minecraft/auth/steps/GetSkinStep.h" + +MSASilent::MSASilent(AccountData* data, QObject* parent) : AuthFlow(data, parent) { + m_steps.append(new MSAStep(m_data, MSAStep::Action::Refresh)); + m_steps.append(new XboxUserStep(m_data)); + m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); + m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); + m_steps.append(new LauncherLoginStep(m_data)); + m_steps.append(new XboxProfileStep(m_data)); + m_steps.append(new EntitlementsStep(m_data)); + m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} + +MSAInteractive::MSAInteractive( + AccountData* data, + QObject* parent +) : AuthFlow(data, parent) { + m_steps.append(new MSAStep(m_data, MSAStep::Action::Login)); + m_steps.append(new XboxUserStep(m_data)); + m_steps.append(new XboxAuthorizationStep(m_data, &m_data->xboxApiToken, "http://xboxlive.com", "Xbox")); + m_steps.append(new XboxAuthorizationStep(m_data, &m_data->mojangservicesToken, "rp://api.minecraftservices.com/", "Mojang")); + m_steps.append(new LauncherLoginStep(m_data)); + m_steps.append(new XboxProfileStep(m_data)); + m_steps.append(new EntitlementsStep(m_data)); + m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} diff --git a/ultimmc/launcher/minecraft/auth/flows/MSA.h b/ultimmc/launcher/minecraft/auth/flows/MSA.h new file mode 100644 index 0000000..14a4ff4 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/flows/MSA.h @@ -0,0 +1,22 @@ +#pragma once +#include "AuthFlow.h" + +class MSAInteractive : public AuthFlow +{ + Q_OBJECT +public: + explicit MSAInteractive( + AccountData *data, + QObject *parent = 0 + ); +}; + +class MSASilent : public AuthFlow +{ + Q_OBJECT +public: + explicit MSASilent( + AccountData * data, + QObject *parent = 0 + ); +}; diff --git a/ultimmc/launcher/minecraft/auth/flows/Mojang.cpp b/ultimmc/launcher/minecraft/auth/flows/Mojang.cpp new file mode 100644 index 0000000..c60bb85 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/flows/Mojang.cpp @@ -0,0 +1,30 @@ +#include "Mojang.h" + +#include "minecraft/auth/steps/YggdrasilStep.h" +#include "minecraft/auth/steps/MinecraftProfileStep.h" +#include "minecraft/auth/steps/ForcedMigrationStep.h" +#include "minecraft/auth/steps/MigrationEligibilityStep.h" +#include "minecraft/auth/steps/GetSkinStep.h" + +MojangRefresh::MojangRefresh( + AccountData *data, + QObject *parent +) : AuthFlow(data, parent) { + m_steps.append(new YggdrasilStep(m_data, QString())); + m_steps.append(new ForcedMigrationStep(m_data)); + m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new MigrationEligibilityStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} + +MojangLogin::MojangLogin( + AccountData *data, + QString password, + QObject *parent +): AuthFlow(data, parent), m_password(password) { + m_steps.append(new YggdrasilStep(m_data, m_password)); + m_steps.append(new ForcedMigrationStep(m_data)); + m_steps.append(new MinecraftProfileStep(m_data)); + m_steps.append(new MigrationEligibilityStep(m_data)); + m_steps.append(new GetSkinStep(m_data)); +} diff --git a/ultimmc/launcher/minecraft/auth/flows/Mojang.h b/ultimmc/launcher/minecraft/auth/flows/Mojang.h new file mode 100644 index 0000000..c09c81a --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/flows/Mojang.h @@ -0,0 +1,26 @@ +#pragma once +#include "AuthFlow.h" + +class MojangRefresh : public AuthFlow +{ + Q_OBJECT +public: + explicit MojangRefresh( + AccountData *data, + QObject *parent = 0 + ); +}; + +class MojangLogin : public AuthFlow +{ + Q_OBJECT +public: + explicit MojangLogin( + AccountData *data, + QString password, + QObject *parent = 0 + ); + +private: + QString m_password; +}; diff --git a/ultimmc/launcher/minecraft/auth/providers/BaseAuthProvider.h b/ultimmc/launcher/minecraft/auth/providers/BaseAuthProvider.h new file mode 100644 index 0000000..7ac0b02 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/providers/BaseAuthProvider.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include "QObjectPtr.h" +#include +#include +#include +#include +#include "../../../AuthServer.h" + +class BaseAuthProvider; +typedef std::shared_ptr AuthProviderPtr; + +/*! + * \brief Base class for auth provider. + * This class implements many functions that are common between providers and + * provides a standard interface for all providers. + * + * To create a new provider, create a new class inheriting from this class, + * implement the pure virtual functions, and + */ +class BaseAuthProvider : public QObject +{ + Q_OBJECT + +public: + virtual ~BaseAuthProvider(){}; + + // Unique id for provider + virtual QString id() + { + return "base"; + }; + + // Name of provider that displayed in account selector and list + virtual QString displayName() + { + return "Base"; + }; + + // Endpoint for authlib injector (use empty if authlib injector isn't required) + virtual QString injectorEndpoint() + { + return ""; + }; + + // Endpoint for authentication + virtual QString authEndpoint() + { + return ""; + }; + + // Function to get url of skin to display in launcher + virtual QUrl resolveSkinUrl(QString id, QString name) + { + return QUrl(((QString) "https://crafatar.com/skins/%1.png").arg(id)); + }; + + // Can change skin (currently only mojang support) + virtual bool canChangeSkin() + { + return false; + } + + // Use legacy yggdrasil auth, (get profile from refresh and login) + virtual bool useYggdrasil() + { + return false; + } + + bool setAuthServer(std::shared_ptr authServer) + { + m_authServer = authServer; + return true; + } + +protected: + std::shared_ptr m_authServer; +}; diff --git a/ultimmc/launcher/minecraft/auth/providers/ElybyAuthProvider.h b/ultimmc/launcher/minecraft/auth/providers/ElybyAuthProvider.h new file mode 100644 index 0000000..00d66bf --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/providers/ElybyAuthProvider.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include "QObjectPtr.h" +#include +#include +#include +#include + +#include "BaseAuthProvider.h" + +class ElybyAuthProvider : public BaseAuthProvider +{ + Q_OBJECT + +public: + QString id() + { + return "elyby"; + } + + QString displayName() + { + return "Ely.by"; + }; + + QString injectorEndpoint() + { + return "ely.by"; + }; + + QString authEndpoint() + { + return "https://authserver.ely.by/auth/"; + }; + + QUrl resolveSkinUrl(QString id, QString name) + { + return QUrl(((QString) "http://skinsystem.ely.by/skins/%1.png").arg(name)); + } + + virtual bool useYggdrasil() + { + return true; + } +}; diff --git a/ultimmc/launcher/minecraft/auth/providers/LocalAuthProvider.h b/ultimmc/launcher/minecraft/auth/providers/LocalAuthProvider.h new file mode 100644 index 0000000..b6a6f7d --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/providers/LocalAuthProvider.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include "QObjectPtr.h" +#include +#include +#include +#include + +#include "BaseAuthProvider.h" + +class LocalAuthProvider : public BaseAuthProvider +{ + Q_OBJECT + +public: + QString id() + { + return "local"; + } + + QString displayName() + { + return "Local"; + } + + bool localAuth() + { + return true; + } + + QString injectorEndpoint() + { + return ((QString)"http://127.0.0.1:%1").arg(m_authServer->port()); + }; + + QString authEndpoint() + { + return ((QString) "http://127.0.0.1:%1/auth/").arg(m_authServer->port()); + }; + + virtual bool useYggdrasil() + { + return true; + } +}; diff --git a/ultimmc/launcher/minecraft/auth/providers/MicrosoftAuthProvider.h b/ultimmc/launcher/minecraft/auth/providers/MicrosoftAuthProvider.h new file mode 100644 index 0000000..8b0908e --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/providers/MicrosoftAuthProvider.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include "QObjectPtr.h" +#include +#include +#include +#include + +#include "MojangAuthProvider.h" + +class MicrosoftAuthProvider : public MojangAuthProvider +{ + Q_OBJECT + +public: + QString id() + { + return "MSA"; + } + + QString displayName() + { + return "Microsoft"; + } +}; diff --git a/ultimmc/launcher/minecraft/auth/providers/MojangAuthProvider.h b/ultimmc/launcher/minecraft/auth/providers/MojangAuthProvider.h new file mode 100644 index 0000000..b52f569 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/providers/MojangAuthProvider.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include "QObjectPtr.h" +#include +#include +#include +#include + +#include "BaseAuthProvider.h" + +class MojangAuthProvider : public BaseAuthProvider +{ + Q_OBJECT + +public: + QString id() + { + return "mojang"; + } + + QString displayName() + { + return "Mojang"; + }; + + QString authEndpoint() + { + return "https://authserver.mojang.com/"; + }; + + bool canChangeSkin() + { + return true; + }; +}; diff --git a/ultimmc/launcher/minecraft/auth/steps/ElybyProfileStep.cpp b/ultimmc/launcher/minecraft/auth/steps/ElybyProfileStep.cpp new file mode 100644 index 0000000..9499266 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/ElybyProfileStep.cpp @@ -0,0 +1,72 @@ +#include "ElybyProfileStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +ElybyProfileStep::ElybyProfileStep(AccountData* data) : AuthStep(data) { + +} + +ElybyProfileStep::~ElybyProfileStep() noexcept = default; + +QString ElybyProfileStep::describe() { + return tr("Fetching the Ely.by profile."); +} + + +void ElybyProfileStep::perform() { + auto url = QUrl(QString("https://authserver.ely.by/api/users/profiles/minecraft/%1").arg(m_data->userName()).toUtf8()); + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &ElybyProfileStep::onRequestDone); + requestor->get(request); +} + +void ElybyProfileStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void ElybyProfileStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + if (error != QNetworkReply::NoError) { + qWarning() << "Error getting profile:"; + qWarning() << " HTTP Status: " << requestor->httpStatus_; + qWarning() << " Internal error no.: " << error; + qWarning() << " Error string: " << requestor->errorString_; + + qWarning() << " Response:"; + qWarning() << QString::fromUtf8(data); + + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile acquisition failed.") + ); + return; + } + if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) { + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile response could not be parsed") + ); + return; + } + + emit finished( + AccountTaskState::STATE_WORKING, + tr("Minecraft Java profile acquisition succeeded.") + ); +} diff --git a/ultimmc/launcher/minecraft/auth/steps/ElybyProfileStep.h b/ultimmc/launcher/minecraft/auth/steps/ElybyProfileStep.h new file mode 100644 index 0000000..765d79e --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/ElybyProfileStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class ElybyProfileStep : public AuthStep { + Q_OBJECT + +public: + explicit ElybyProfileStep(AccountData *data); + virtual ~ElybyProfileStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/ultimmc/launcher/minecraft/auth/steps/EntitlementsStep.cpp b/ultimmc/launcher/minecraft/auth/steps/EntitlementsStep.cpp new file mode 100644 index 0000000..f726244 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/EntitlementsStep.cpp @@ -0,0 +1,53 @@ +#include "EntitlementsStep.h" + +#include +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +EntitlementsStep::EntitlementsStep(AccountData* data) : AuthStep(data) {} + +EntitlementsStep::~EntitlementsStep() noexcept = default; + +QString EntitlementsStep::describe() { + return tr("Determining game ownership."); +} + + +void EntitlementsStep::perform() { + auto uuid = QUuid::createUuid(); + m_entitlementsRequestId = uuid.toString().remove('{').remove('}'); + auto url = "https://api.minecraftservices.com/entitlements/license?requestId=" + m_entitlementsRequestId; + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &EntitlementsStep::onRequestDone); + requestor->get(request); + qDebug() << "Getting entitlements..."; +} + +void EntitlementsStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void EntitlementsStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + + // TODO: check presence of same entitlementsRequestId? + // TODO: validate JWTs? + Parsers::parseMinecraftEntitlements(data, m_data->minecraftEntitlement); + + emit finished(AccountTaskState::STATE_WORKING, tr("Got entitlements")); +} diff --git a/ultimmc/launcher/minecraft/auth/steps/EntitlementsStep.h b/ultimmc/launcher/minecraft/auth/steps/EntitlementsStep.h new file mode 100644 index 0000000..9412ae7 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/EntitlementsStep.h @@ -0,0 +1,25 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class EntitlementsStep : public AuthStep { + Q_OBJECT + +public: + explicit EntitlementsStep(AccountData *data); + virtual ~EntitlementsStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); + +private: + QString m_entitlementsRequestId; +}; diff --git a/ultimmc/launcher/minecraft/auth/steps/ForcedMigrationStep.cpp b/ultimmc/launcher/minecraft/auth/steps/ForcedMigrationStep.cpp new file mode 100644 index 0000000..2e816c9 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/ForcedMigrationStep.cpp @@ -0,0 +1,52 @@ +#include "ForcedMigrationStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +ForcedMigrationStep::ForcedMigrationStep(AccountData* data) : AuthStep(data) { + +} + +ForcedMigrationStep::~ForcedMigrationStep() noexcept = default; + +QString ForcedMigrationStep::describe() { + return tr("Checking for migration eligibility."); +} + +void ForcedMigrationStep::perform() { + auto url = QUrl("https://api.minecraftservices.com/rollout/v1/msamigrationforced"); + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); + + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &ForcedMigrationStep::onRequestDone); + requestor->get(request); +} + +void ForcedMigrationStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void ForcedMigrationStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + + if (error == QNetworkReply::NoError) { + Parsers::parseForcedMigrationResponse(data, m_data->mustMigrateToMSA); + } + if(m_data->mustMigrateToMSA) { + emit finished(AccountTaskState::STATE_FAILED_MUST_MIGRATE, tr("The account must be migrated to a Microsoft account.")); + } + else { + emit finished(AccountTaskState::STATE_WORKING, tr("Got forced migration flags")); + } + +} + diff --git a/ultimmc/launcher/minecraft/auth/steps/ForcedMigrationStep.h b/ultimmc/launcher/minecraft/auth/steps/ForcedMigrationStep.h new file mode 100644 index 0000000..8b9cbbb --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/ForcedMigrationStep.h @@ -0,0 +1,23 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class ForcedMigrationStep : public AuthStep { + Q_OBJECT + +public: + explicit ForcedMigrationStep(AccountData *data); + virtual ~ForcedMigrationStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; + diff --git a/ultimmc/launcher/minecraft/auth/steps/GetSkinStep.cpp b/ultimmc/launcher/minecraft/auth/steps/GetSkinStep.cpp new file mode 100644 index 0000000..3521f8d --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/GetSkinStep.cpp @@ -0,0 +1,43 @@ + +#include "GetSkinStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +GetSkinStep::GetSkinStep(AccountData* data) : AuthStep(data) { + +} + +GetSkinStep::~GetSkinStep() noexcept = default; + +QString GetSkinStep::describe() { + return tr("Getting skin."); +} + +void GetSkinStep::perform() { + auto url = QUrl(m_data->minecraftProfile.skin.url); + QNetworkRequest request = QNetworkRequest(url); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &GetSkinStep::onRequestDone); + requestor->get(request); +} + +void GetSkinStep::rehydrate() { + // NOOP, for now. +} + +void GetSkinStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + + if (error == QNetworkReply::NoError) { + m_data->minecraftProfile.skin.data = data; + } + emit finished(AccountTaskState::STATE_SUCCEEDED, tr("Got skin")); +} diff --git a/ultimmc/launcher/minecraft/auth/steps/GetSkinStep.h b/ultimmc/launcher/minecraft/auth/steps/GetSkinStep.h new file mode 100644 index 0000000..6b97371 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/GetSkinStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class GetSkinStep : public AuthStep { + Q_OBJECT + +public: + explicit GetSkinStep(AccountData *data); + virtual ~GetSkinStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/ultimmc/launcher/minecraft/auth/steps/LauncherLoginStep.cpp b/ultimmc/launcher/minecraft/auth/steps/LauncherLoginStep.cpp new file mode 100644 index 0000000..a80cb2b --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/LauncherLoginStep.cpp @@ -0,0 +1,77 @@ +#include "LauncherLoginStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" +#include "minecraft/auth/AccountTask.h" + +LauncherLoginStep::LauncherLoginStep(AccountData* data) : AuthStep(data) { + +} + +LauncherLoginStep::~LauncherLoginStep() noexcept = default; + +QString LauncherLoginStep::describe() { + return tr("Accessing Mojang services."); +} + +void LauncherLoginStep::perform() { + auto requestURL = "https://api.minecraftservices.com/launcher/login"; + auto uhs = m_data->mojangservicesToken.extra["uhs"].toString(); + auto xToken = m_data->mojangservicesToken.token; + + QString mc_auth_template = R"XXX( +{ + "xtoken": "XBL3.0 x=%1;%2", + "platform": "PC_LAUNCHER" +} +)XXX"; + auto requestBody = mc_auth_template.arg(uhs, xToken); + + QNetworkRequest request = QNetworkRequest(QUrl(requestURL)); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &LauncherLoginStep::onRequestDone); + requestor->post(request, requestBody.toUtf8()); + qDebug() << "Getting Minecraft access token..."; +} + +void LauncherLoginStep::rehydrate() { + // TODO: check the token validity +} + +void LauncherLoginStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; +#ifndef NDEBUG + qDebug() << data; +#endif + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to get Minecraft access token: %1").arg(requestor->errorString_) + ); + return; + } + + if(!Parsers::parseMojangResponse(data, m_data->yggdrasilToken)) { + qWarning() << "Could not parse login_with_xbox response..."; +#ifndef NDEBUG + qDebug() << data; +#endif + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to parse the Minecraft access token response.") + ); + return; + } + emit finished(AccountTaskState::STATE_WORKING, tr("")); +} diff --git a/ultimmc/launcher/minecraft/auth/steps/LauncherLoginStep.h b/ultimmc/launcher/minecraft/auth/steps/LauncherLoginStep.h new file mode 100644 index 0000000..e06a306 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/LauncherLoginStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class LauncherLoginStep : public AuthStep { + Q_OBJECT + +public: + explicit LauncherLoginStep(AccountData *data); + virtual ~LauncherLoginStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/ultimmc/launcher/minecraft/auth/steps/LocalStep.cpp b/ultimmc/launcher/minecraft/auth/steps/LocalStep.cpp new file mode 100644 index 0000000..61429ca --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/LocalStep.cpp @@ -0,0 +1,23 @@ +#include "LocalStep.h" + +#include "Application.h" + +// This step does nothing but just log that we created a local account. + +LocalStep::LocalStep(AccountData* data) : AuthStep(data) {} +LocalStep::~LocalStep() noexcept = default; + +QString LocalStep::describe() +{ + return tr("Creating local account."); +} + +void LocalStep::rehydrate() +{ + // NOOP +} + +void LocalStep::perform() +{ + emit finished(AccountTaskState::STATE_WORKING, tr("Created local account.")); +} diff --git a/ultimmc/launcher/minecraft/auth/steps/LocalStep.h b/ultimmc/launcher/minecraft/auth/steps/LocalStep.h new file mode 100644 index 0000000..bc24260 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/LocalStep.h @@ -0,0 +1,19 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + +#include + +class LocalStep : public AuthStep { + Q_OBJECT + public: + explicit LocalStep(AccountData* data); + virtual ~LocalStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; +}; diff --git a/ultimmc/launcher/minecraft/auth/steps/MSAStep.cpp b/ultimmc/launcher/minecraft/auth/steps/MSAStep.cpp new file mode 100644 index 0000000..be711f7 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/MSAStep.cpp @@ -0,0 +1,111 @@ +#include "MSAStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +#include "Application.h" + +using OAuth2 = Katabasis::DeviceFlow; +using Activity = Katabasis::Activity; + +MSAStep::MSAStep(AccountData* data, Action action) : AuthStep(data), m_action(action) { + OAuth2::Options opts; + opts.scope = "XboxLive.signin offline_access"; + opts.clientIdentifier = APPLICATION->msaClientId(); + opts.authorizationUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode"; + opts.accessTokenUrl = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"; + + // FIXME: OAuth2 is not aware of our fancy shared pointers + m_oauth2 = new OAuth2(opts, m_data->msaToken, this, APPLICATION->network().get()); + + connect(m_oauth2, &OAuth2::activityChanged, this, &MSAStep::onOAuthActivityChanged); + connect(m_oauth2, &OAuth2::showVerificationUriAndCode, this, &MSAStep::showVerificationUriAndCode); +} + +MSAStep::~MSAStep() noexcept = default; + +QString MSAStep::describe() { + return tr("Logging in with Microsoft account."); +} + + +void MSAStep::rehydrate() { + switch(m_action) { + case Refresh: { + // TODO: check the tokens and see if they are old (older than a day) + return; + } + case Login: { + // NOOP + return; + } + } +} + +void MSAStep::perform() { + switch(m_action) { + case Refresh: { + m_oauth2->refresh(); + return; + } + case Login: { + QVariantMap extraOpts; + extraOpts["prompt"] = "select_account"; + m_oauth2->setExtraRequestParams(extraOpts); + + *m_data = AccountData(); + m_oauth2->login(); + return; + } + } +} + +void MSAStep::onOAuthActivityChanged(Katabasis::Activity activity) { + switch(activity) { + case Katabasis::Activity::Idle: + case Katabasis::Activity::LoggingIn: + case Katabasis::Activity::Refreshing: + case Katabasis::Activity::LoggingOut: { + // We asked it to do something, it's doing it. Nothing to act upon. + return; + } + case Katabasis::Activity::Succeeded: { + // Succeeded or did not invalidate tokens + emit hideVerificationUriAndCode(); + QVariantMap extraTokens = m_oauth2->extraTokens(); +#ifndef NDEBUG + if (!extraTokens.isEmpty()) { + qDebug() << "Extra tokens in response:"; + foreach (QString key, extraTokens.keys()) { + qDebug() << "\t" << key << ":" << extraTokens.value(key); + } + } +#endif + emit finished(AccountTaskState::STATE_WORKING, tr("Got ")); + return; + } + case Katabasis::Activity::FailedSoft: { + // NOTE: soft error in the first step means 'offline' + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_OFFLINE, tr("Microsoft user authentication ended with a network error.")); + return; + } + case Katabasis::Activity::FailedGone: { + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_FAILED_GONE, tr("Microsoft user authentication failed - user no longer exists.")); + return; + } + case Katabasis::Activity::FailedHard: { + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication failed.")); + return; + } + default: { + emit hideVerificationUriAndCode(); + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("Microsoft user authentication completed with an unrecognized result.")); + return; + } + } +} diff --git a/ultimmc/launcher/minecraft/auth/steps/MSAStep.h b/ultimmc/launcher/minecraft/auth/steps/MSAStep.h new file mode 100644 index 0000000..49ba354 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/MSAStep.h @@ -0,0 +1,32 @@ + +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + +#include + +class MSAStep : public AuthStep { + Q_OBJECT +public: + enum Action { + Refresh, + Login + }; +public: + explicit MSAStep(AccountData *data, Action action); + virtual ~MSAStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onOAuthActivityChanged(Katabasis::Activity activity); + +private: + Katabasis::DeviceFlow *m_oauth2 = nullptr; + Action m_action; +}; diff --git a/ultimmc/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp b/ultimmc/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp new file mode 100644 index 0000000..f5b5637 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/MigrationEligibilityStep.cpp @@ -0,0 +1,45 @@ +#include "MigrationEligibilityStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +MigrationEligibilityStep::MigrationEligibilityStep(AccountData* data) : AuthStep(data) { + +} + +MigrationEligibilityStep::~MigrationEligibilityStep() noexcept = default; + +QString MigrationEligibilityStep::describe() { + return tr("Checking for migration eligibility."); +} + +void MigrationEligibilityStep::perform() { + auto url = QUrl("https://api.minecraftservices.com/rollout/v1/msamigration"); + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); + + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &MigrationEligibilityStep::onRequestDone); + requestor->get(request); +} + +void MigrationEligibilityStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void MigrationEligibilityStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + + if (error == QNetworkReply::NoError) { + Parsers::parseRolloutResponse(data, m_data->canMigrateToMSA); + } + emit finished(AccountTaskState::STATE_WORKING, tr("Got migration flags")); +} diff --git a/ultimmc/launcher/minecraft/auth/steps/MigrationEligibilityStep.h b/ultimmc/launcher/minecraft/auth/steps/MigrationEligibilityStep.h new file mode 100644 index 0000000..b1bf9cb --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/MigrationEligibilityStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class MigrationEligibilityStep : public AuthStep { + Q_OBJECT + +public: + explicit MigrationEligibilityStep(AccountData *data); + virtual ~MigrationEligibilityStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/ultimmc/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp b/ultimmc/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp new file mode 100644 index 0000000..add9165 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/MinecraftProfileStep.cpp @@ -0,0 +1,91 @@ +#include "MinecraftProfileStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +MinecraftProfileStep::MinecraftProfileStep(AccountData* data) : AuthStep(data) { + +} + +MinecraftProfileStep::~MinecraftProfileStep() noexcept = default; + +QString MinecraftProfileStep::describe() { + return tr("Fetching the Minecraft profile."); +} + + +void MinecraftProfileStep::perform() { + auto url = QUrl("https://api.minecraftservices.com/minecraft/profile"); + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_data->yggdrasilToken.token).toUtf8()); + + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &MinecraftProfileStep::onRequestDone); + requestor->get(request); +} + +void MinecraftProfileStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void MinecraftProfileStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + if (error == QNetworkReply::ContentNotFoundError) { + // NOTE: Succeed even if we do not have a profile. This is a valid account state. + if(m_data->type == AccountType::Mojang) { + m_data->minecraftEntitlement.canPlayMinecraft = false; + m_data->minecraftEntitlement.ownsMinecraft = false; + } + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_SUCCEEDED, + tr("Account has no Minecraft profile.") + ); + return; + } + if (error != QNetworkReply::NoError) { + qWarning() << "Error getting profile:"; + qWarning() << " HTTP Status: " << requestor->httpStatus_; + qWarning() << " Internal error no.: " << error; + qWarning() << " Error string: " << requestor->errorString_; + + qWarning() << " Response:"; + qWarning() << QString::fromUtf8(data); + + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile acquisition failed.") + ); + return; + } + if(!Parsers::parseMinecraftProfile(data, m_data->minecraftProfile)) { + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile response could not be parsed") + ); + return; + } + + if(m_data->type == AccountType::Mojang) { + auto validProfile = m_data->minecraftProfile.validity == Katabasis::Validity::Certain; + m_data->minecraftEntitlement.canPlayMinecraft = validProfile; + m_data->minecraftEntitlement.ownsMinecraft = validProfile; + } + emit finished( + AccountTaskState::STATE_WORKING, + tr("Minecraft Java profile acquisition succeeded.") + ); +} diff --git a/ultimmc/launcher/minecraft/auth/steps/MinecraftProfileStep.h b/ultimmc/launcher/minecraft/auth/steps/MinecraftProfileStep.h new file mode 100644 index 0000000..8ef3395 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/MinecraftProfileStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class MinecraftProfileStep : public AuthStep { + Q_OBJECT + +public: + explicit MinecraftProfileStep(AccountData *data); + virtual ~MinecraftProfileStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/ultimmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp b/ultimmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp new file mode 100644 index 0000000..b4147f1 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.cpp @@ -0,0 +1,194 @@ +#include "XboxAuthorizationStep.h" + +#include +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +XboxAuthorizationStep::XboxAuthorizationStep(AccountData* data, Katabasis::Token *token, QString relyingParty, QString authorizationKind): + AuthStep(data), + m_token(token), + m_relyingParty(relyingParty), + m_authorizationKind(authorizationKind) +{ +} + +XboxAuthorizationStep::~XboxAuthorizationStep() noexcept = default; + +QString XboxAuthorizationStep::describe() { + return tr("Getting authorization to access %1 services.").arg(m_authorizationKind); +} + +void XboxAuthorizationStep::rehydrate() { + // FIXME: check if the tokens are good? +} + +void XboxAuthorizationStep::perform() { + QString xbox_auth_template = R"XXX( +{ + "Properties": { + "SandboxId": "RETAIL", + "UserTokens": [ + "%1" + ] + }, + "RelyingParty": "%2", + "TokenType": "JWT" +} +)XXX"; + auto xbox_auth_data = xbox_auth_template.arg(m_data->userToken.token, m_relyingParty); +// http://xboxlive.com + QNetworkRequest request = QNetworkRequest(QUrl("https://xsts.auth.xboxlive.com/xsts/authorize")); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &XboxAuthorizationStep::onRequestDone); + requestor->post(request, xbox_auth_data.toUtf8()); + qDebug() << "Getting authorization token for " << m_relyingParty; +} + +void XboxAuthorizationStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; + if(!processSTSError(error, data, headers)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to get authorization for %1 services. Error %2.").arg(m_authorizationKind, error) + ); + } + return; + } + + Katabasis::Token temp; + if(!Parsers::parseXTokenResponse(data, temp, m_authorizationKind)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Could not parse authorization response for access to %1 services.").arg(m_authorizationKind) + ); + return; + } + + if(temp.extra["uhs"] != m_data->userToken.extra["uhs"]) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Server has changed %1 authorization user hash in the reply. Something is wrong.").arg(m_authorizationKind) + ); + return; + } + auto & token = *m_token; + token = temp; + + emit finished(AccountTaskState::STATE_WORKING, tr("Got authorization to access %1").arg(m_relyingParty)); +} + + +bool XboxAuthorizationStep::processSTSError( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + if(error == QNetworkReply::AuthenticationRequiredError) { + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if(jsonError.error) { + qWarning() << "Cannot parse error XSTS response as JSON: " << jsonError.errorString(); + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Cannot parse %1 authorization error response as JSON: %2").arg(m_authorizationKind, jsonError.errorString()) + ); + return true; + } + + int64_t errorCode = -1; + auto obj = doc.object(); + if(!Parsers::getNumber(obj.value("XErr"), errorCode)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("XErr element is missing from %1 authorization error response.").arg(m_authorizationKind) + ); + return true; + } + switch(errorCode) { + case 2148916227: { + // NOTE: this is the error experienced by a number of people on Discord using dodgy alt accounts + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Your XBox Live account has been banned by Microsoft for violating the XBox Community Standards.\nThis may happen if your account was shared or resold.") + ); + return true; + } + case 2148916229: { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("This Microsoft account is linked to a family and your parent or guardian has not given you permission to play online.") + ); + return true; + } + case 2148916233: { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("This Microsoft account does not have an XBox Live profile. Buy the game on %1 first.") + .arg("minecraft.net") + ); + return true; + } + case 2148916234: { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("This account has not accepted the XBox Terms of Service. Please log in online and accept them.") + ); + return true; + } + case 2148916235: { + // NOTE: this is the Grulovia error + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("XBox Live is not available in your country. You've been blocked.") + ); + return true; + } + case 2148916236: { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("This Microsoft account requires proof of age to play. Please login to %1 to provide proof of age.") + .arg("login.live.com") + ); + return true; + } + case 2148916237: { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("This Microsoft account has reached its playtime limit and has been blocked from logging in.") + ); + return true; + } + case 2148916238: { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("This Microsoft account is underaged and is not linked to a family.\n\nPlease set up your account according to %1.") + .arg("help.minecraft.net") + ); + return true; + } + default: { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("XSTS authentication ended with unrecognized error(s):\n\n%1").arg(errorCode) + ); + return true; + } + } + } + return false; +} diff --git a/ultimmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.h b/ultimmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.h new file mode 100644 index 0000000..31e43bf --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/XboxAuthorizationStep.h @@ -0,0 +1,34 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class XboxAuthorizationStep : public AuthStep { + Q_OBJECT + +public: + explicit XboxAuthorizationStep(AccountData *data, Katabasis::Token *token, QString relyingParty, QString authorizationKind); + virtual ~XboxAuthorizationStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private: + bool processSTSError( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers + ); + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); + +private: + Katabasis::Token *m_token; + QString m_relyingParty; + QString m_authorizationKind; +}; diff --git a/ultimmc/launcher/minecraft/auth/steps/XboxProfileStep.cpp b/ultimmc/launcher/minecraft/auth/steps/XboxProfileStep.cpp new file mode 100644 index 0000000..9f50138 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/XboxProfileStep.cpp @@ -0,0 +1,73 @@ +#include "XboxProfileStep.h" + +#include +#include + + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +XboxProfileStep::XboxProfileStep(AccountData* data) : AuthStep(data) { + +} + +XboxProfileStep::~XboxProfileStep() noexcept = default; + +QString XboxProfileStep::describe() { + return tr("Fetching Xbox profile."); +} + +void XboxProfileStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void XboxProfileStep::perform() { + auto url = QUrl("https://profile.xboxlive.com/users/me/profile/settings"); + QUrlQuery q; + q.addQueryItem( + "settings", + "GameDisplayName,AppDisplayName,AppDisplayPicRaw,GameDisplayPicRaw," + "PublicGamerpic,ShowUserAsAvatar,Gamerscore,Gamertag,ModernGamertag,ModernGamertagSuffix," + "UniqueModernGamertag,AccountTier,TenureLevel,XboxOneRep," + "PreferredColor,Location,Bio,Watermarks," + "RealName,RealNameOverride,IsQuarantined" + ); + url.setQuery(q); + + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + request.setRawHeader("x-xbl-contract-version", "3"); + request.setRawHeader("Authorization", QString("XBL3.0 x=%1;%2").arg(m_data->userToken.extra["uhs"].toString(), m_data->xboxApiToken.token).toUtf8()); + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &XboxProfileStep::onRequestDone); + requestor->get(request); + qDebug() << "Getting Xbox profile..."; +} + +void XboxProfileStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; +#ifndef NDEBUG + qDebug() << data; +#endif + finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Failed to retrieve the Xbox profile.") + ); + return; + } + +#ifndef NDEBUG + qDebug() << "XBox profile: " << data; +#endif + + emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox profile")); +} diff --git a/ultimmc/launcher/minecraft/auth/steps/XboxProfileStep.h b/ultimmc/launcher/minecraft/auth/steps/XboxProfileStep.h new file mode 100644 index 0000000..7a0c587 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/XboxProfileStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class XboxProfileStep : public AuthStep { + Q_OBJECT + +public: + explicit XboxProfileStep(AccountData *data); + virtual ~XboxProfileStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/ultimmc/launcher/minecraft/auth/steps/XboxUserStep.cpp b/ultimmc/launcher/minecraft/auth/steps/XboxUserStep.cpp new file mode 100644 index 0000000..a38a28e --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/XboxUserStep.cpp @@ -0,0 +1,68 @@ +#include "XboxUserStep.h" + +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + +XboxUserStep::XboxUserStep(AccountData* data) : AuthStep(data) { + +} + +XboxUserStep::~XboxUserStep() noexcept = default; + +QString XboxUserStep::describe() { + return tr("Logging in as an Xbox user."); +} + + +void XboxUserStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void XboxUserStep::perform() { + QString xbox_auth_template = R"XXX( +{ + "Properties": { + "AuthMethod": "RPS", + "SiteName": "user.auth.xboxlive.com", + "RpsTicket": "d=%1" + }, + "RelyingParty": "http://auth.xboxlive.com", + "TokenType": "JWT" +} +)XXX"; + auto xbox_auth_data = xbox_auth_template.arg(m_data->msaToken.token); + + QNetworkRequest request = QNetworkRequest(QUrl("https://user.auth.xboxlive.com/user/authenticate")); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + auto *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &XboxUserStep::onRequestDone); + requestor->post(request, xbox_auth_data.toUtf8()); + qDebug() << "First layer of XBox auth ... commencing."; +} + +void XboxUserStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + + if (error != QNetworkReply::NoError) { + qWarning() << "Reply error:" << error; + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication failed.")); + return; + } + + Katabasis::Token temp; + if(!Parsers::parseXTokenResponse(data, temp, "UToken")) { + qWarning() << "Could not parse user authentication response..."; + emit finished(AccountTaskState::STATE_FAILED_SOFT, tr("XBox user authentication response could not be understood.")); + return; + } + m_data->userToken = temp; + emit finished(AccountTaskState::STATE_WORKING, tr("Got Xbox user token")); +} diff --git a/ultimmc/launcher/minecraft/auth/steps/XboxUserStep.h b/ultimmc/launcher/minecraft/auth/steps/XboxUserStep.h new file mode 100644 index 0000000..83e9405 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/XboxUserStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class XboxUserStep : public AuthStep { + Q_OBJECT + +public: + explicit XboxUserStep(AccountData *data); + virtual ~XboxUserStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/ultimmc/launcher/minecraft/auth/steps/YggdrasilStep.cpp b/ultimmc/launcher/minecraft/auth/steps/YggdrasilStep.cpp new file mode 100644 index 0000000..4c6b162 --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/YggdrasilStep.cpp @@ -0,0 +1,51 @@ +#include "YggdrasilStep.h" + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" +#include "minecraft/auth/Yggdrasil.h" + +YggdrasilStep::YggdrasilStep(AccountData* data, QString password) : AuthStep(data), m_password(password) { + m_yggdrasil = new Yggdrasil(m_data, this); + + connect(m_yggdrasil, &Task::failed, this, &YggdrasilStep::onAuthFailed); + connect(m_yggdrasil, &Task::succeeded, this, &YggdrasilStep::onAuthSucceeded); +} + +YggdrasilStep::~YggdrasilStep() noexcept = default; + +QString YggdrasilStep::describe() { + return tr("Logging in with Mojang account."); +} + +void YggdrasilStep::rehydrate() { + // NOOP, for now. +} + +void YggdrasilStep::perform() { + if(m_password.size()) { + m_yggdrasil->login(m_password); + } + else { + m_yggdrasil->refresh(); + } +} + +void YggdrasilStep::onAuthSucceeded() { + emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Mojang")); +} + +void YggdrasilStep::onAuthFailed() { + // TODO: hook these in again, expand to MSA + // m_error = m_yggdrasil->m_error; + // m_aborted = m_yggdrasil->m_aborted; + + auto state = m_yggdrasil->taskState(); + QString errorMessage = tr("Mojang user authentication failed."); + + // NOTE: soft error in the first step means 'offline' + if(state == AccountTaskState::STATE_FAILED_SOFT) { + state = AccountTaskState::STATE_OFFLINE; + errorMessage = tr("Mojang user authentication ended with a network error."); + } + emit finished(state, errorMessage); +} diff --git a/ultimmc/launcher/minecraft/auth/steps/YggdrasilStep.h b/ultimmc/launcher/minecraft/auth/steps/YggdrasilStep.h new file mode 100644 index 0000000..ebafb8e --- /dev/null +++ b/ultimmc/launcher/minecraft/auth/steps/YggdrasilStep.h @@ -0,0 +1,28 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + +class Yggdrasil; + +class YggdrasilStep : public AuthStep { + Q_OBJECT + +public: + explicit YggdrasilStep(AccountData *data, QString password); + virtual ~YggdrasilStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onAuthSucceeded(); + void onAuthFailed(); + +private: + Yggdrasil *m_yggdrasil = nullptr; + QString m_password; +}; diff --git a/ultimmc/launcher/minecraft/gameoptions/GameOptions.cpp b/ultimmc/launcher/minecraft/gameoptions/GameOptions.cpp new file mode 100644 index 0000000..e547b32 --- /dev/null +++ b/ultimmc/launcher/minecraft/gameoptions/GameOptions.cpp @@ -0,0 +1,144 @@ +#include "GameOptions.h" +#include "FileSystem.h" +#include +#include + +namespace { +bool load(const QString& path, std::vector &contents, int & version) +{ + contents.clear(); + QFile file(path); + if (!file.open(QFile::ReadOnly)) + { + qWarning() << "Failed to read options file."; + return false; + } + version = 0; + while(!file.atEnd()) + { + auto line = file.readLine(); + if(line.endsWith('\n')) + { + line.chop(1); + } + auto separatorIndex = line.indexOf(':'); + if(separatorIndex == -1) + { + continue; + } + auto key = QString::fromUtf8(line.data(), separatorIndex); + auto value = QString::fromUtf8(line.data() + separatorIndex + 1, line.size() - 1 - separatorIndex); + qDebug() << "!!" << key << "!!"; + if(key == "version") + { + version = value.toInt(); + continue; + } + contents.emplace_back(GameOptionItem{key, value}); + } + qDebug() << "Loaded" << path << "with version:" << version; + return true; +} +bool save(const QString& path, std::vector &mapping, int version) +{ + QSaveFile out(path); + if(!out.open(QIODevice::WriteOnly)) + { + return false; + } + if(version != 0) + { + QString versionLine = QString("version:%1\n").arg(version); + out.write(versionLine.toUtf8()); + } + auto iter = mapping.begin(); + while (iter != mapping.end()) + { + out.write(iter->key.toUtf8()); + out.write(":"); + out.write(iter->value.toUtf8()); + out.write("\n"); + iter++; + } + return out.commit(); +} +} + +GameOptions::GameOptions(const QString& path): + path(path) +{ + reload(); +} + +QVariant GameOptions::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(role != Qt::DisplayRole) + { + return QAbstractListModel::headerData(section, orientation, role); + } + switch(section) + { + case 0: + return tr("Key"); + case 1: + return tr("Value"); + default: + return QVariant(); + } +} + +QVariant GameOptions::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= int(contents.size())) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + if(column == 0) + { + return contents[row].key; + } + else + { + return contents[row].value; + } + default: + return QVariant(); + } + return QVariant(); +} + +int GameOptions::rowCount(const QModelIndex&) const +{ + return contents.size(); +} + +int GameOptions::columnCount(const QModelIndex&) const +{ + return 2; +} + +bool GameOptions::isLoaded() const +{ + return loaded; +} + +bool GameOptions::reload() +{ + beginResetModel(); + loaded = load(path, contents, version); + endResetModel(); + return loaded; +} + +bool GameOptions::save() +{ + return ::save(path, contents, version); +} diff --git a/ultimmc/launcher/minecraft/gameoptions/GameOptions.h b/ultimmc/launcher/minecraft/gameoptions/GameOptions.h new file mode 100644 index 0000000..c6d2549 --- /dev/null +++ b/ultimmc/launcher/minecraft/gameoptions/GameOptions.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include + +struct GameOptionItem +{ + QString key; + QString value; +}; + +class GameOptions : public QAbstractListModel +{ + Q_OBJECT +public: + explicit GameOptions(const QString& path); + virtual ~GameOptions() = default; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex & parent) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + bool isLoaded() const; + bool reload(); + bool save(); + +private: + std::vector contents; + bool loaded = false; + QString path; + int version = 0; +}; diff --git a/ultimmc/launcher/minecraft/launch/ClaimAccount.cpp b/ultimmc/launcher/minecraft/launch/ClaimAccount.cpp new file mode 100644 index 0000000..1cd7c0d --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/ClaimAccount.cpp @@ -0,0 +1,28 @@ +#include "ClaimAccount.h" +#include + +#include "Application.h" +#include "minecraft/auth/AccountList.h" + +ClaimAccount::ClaimAccount(LaunchTask* parent, AuthSessionPtr session): LaunchStep(parent) +{ + if(session->status == AuthSession::Status::PlayableOnline && !session->demo) + { + auto accounts = APPLICATION->accounts(); + m_account = accounts->getAccountByProfileName(session->player_name); + } +} + +void ClaimAccount::executeTask() +{ + if(m_account) + { + lock.reset(new UseLock(m_account)); + emitSucceeded(); + } +} + +void ClaimAccount::finalize() +{ + lock.reset(); +} diff --git a/ultimmc/launcher/minecraft/launch/ClaimAccount.h b/ultimmc/launcher/minecraft/launch/ClaimAccount.h new file mode 100644 index 0000000..cb4de23 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/ClaimAccount.h @@ -0,0 +1,37 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +class ClaimAccount: public LaunchStep +{ + Q_OBJECT +public: + explicit ClaimAccount(LaunchTask *parent, AuthSessionPtr session); + virtual ~ClaimAccount() {}; + + void executeTask() override; + void finalize() override; + bool canAbort() const override + { + return false; + } +private: + std::unique_ptr lock; + MinecraftAccountPtr m_account; +}; diff --git a/ultimmc/launcher/minecraft/launch/CreateGameFolders.cpp b/ultimmc/launcher/minecraft/launch/CreateGameFolders.cpp new file mode 100644 index 0000000..4081e72 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/CreateGameFolders.cpp @@ -0,0 +1,28 @@ +#include "CreateGameFolders.h" +#include "minecraft/MinecraftInstance.h" +#include "launch/LaunchTask.h" +#include "FileSystem.h" + +CreateGameFolders::CreateGameFolders(LaunchTask* parent): LaunchStep(parent) +{ +} + +void CreateGameFolders::executeTask() +{ + auto instance = m_parent->instance(); + std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); + + if(!FS::ensureFolderPathExists(minecraftInstance->gameRoot())) + { + emit logLine("Couldn't create the main game folder", MessageLevel::Error); + emitFailed(tr("Couldn't create the main game folder")); + return; + } + + // HACK: this is a workaround for MCL-3732 - 'server-resource-packs' folder is created. + if(!FS::ensureFolderPathExists(FS::PathCombine(minecraftInstance->gameRoot(), "server-resource-packs"))) + { + emit logLine("Couldn't create the 'server-resource-packs' folder", MessageLevel::Error); + } + emitSucceeded(); +} diff --git a/ultimmc/launcher/minecraft/launch/CreateGameFolders.h b/ultimmc/launcher/minecraft/launch/CreateGameFolders.h new file mode 100644 index 0000000..9c7d3c9 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/CreateGameFolders.h @@ -0,0 +1,37 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +// Create the main .minecraft for the instance and any other necessary folders +class CreateGameFolders: public LaunchStep +{ + Q_OBJECT +public: + explicit CreateGameFolders(LaunchTask *parent); + virtual ~CreateGameFolders() {}; + + virtual void executeTask(); + virtual bool canAbort() const + { + return false; + } +}; + + diff --git a/ultimmc/launcher/minecraft/launch/DirectJavaLaunch.cpp b/ultimmc/launcher/minecraft/launch/DirectJavaLaunch.cpp new file mode 100644 index 0000000..69218b3 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/DirectJavaLaunch.cpp @@ -0,0 +1,148 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "DirectJavaLaunch.h" +#include +#include +#include +#include +#include + +DirectJavaLaunch::DirectJavaLaunch(LaunchTask *parent) : LaunchStep(parent) +{ + connect(&m_process, &LoggedProcess::log, this, &DirectJavaLaunch::logLines); + connect(&m_process, &LoggedProcess::stateChanged, this, &DirectJavaLaunch::on_state); +} + +void DirectJavaLaunch::executeTask() +{ + auto instance = m_parent->instance(); + std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); + QStringList args = minecraftInstance->javaArguments(); + + args.append("-Djava.library.path=" + minecraftInstance->getNativePath()); + + auto classPathEntries = minecraftInstance->getClassPath(); + args.append("-cp"); + QString classpath; +#ifdef Q_OS_WIN32 + classpath = classPathEntries.join(';'); +#else + classpath = classPathEntries.join(':'); +#endif + args.append(classpath); + args.append(minecraftInstance->getMainClass()); + + QString allArgs = args.join(", "); + emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::Launcher); + + auto javaPath = FS::ResolveExecutable(instance->settings()->get("JavaPath").toString()); + + m_process.setProcessEnvironment(instance->createEnvironment()); + + // make detachable - this will keep the process running even if the object is destroyed + m_process.setDetachable(true); + + auto mcArgs = minecraftInstance->processMinecraftArgs(m_session, m_quickPlayTarget); + args.append(mcArgs); + + QString wrapperCommandStr = instance->getWrapperCommand().trimmed(); + if(!wrapperCommandStr.isEmpty()) + { + auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr); + auto wrapperCommand = wrapperArgs.takeFirst(); + auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); + if (realWrapperCommand.isEmpty()) + { + const char *reason = QT_TR_NOOP("The wrapper command \"%1\" couldn't be found."); + emit logLine(QString(reason).arg(wrapperCommand), MessageLevel::Fatal); + emitFailed(tr(reason).arg(wrapperCommand)); + return; + } + emit logLine("Wrapper command is:\n" + wrapperCommandStr + "\n\n", MessageLevel::Launcher); + args.prepend(javaPath); + m_process.start(wrapperCommand, wrapperArgs + args); + } + else + { + m_process.start(javaPath, args); + } +} + +void DirectJavaLaunch::on_state(LoggedProcess::State state) +{ + switch(state) + { + case LoggedProcess::FailedToStart: + { + //: Error message displayed if instance can't start + const char *reason = QT_TR_NOOP("Could not launch minecraft!"); + emit logLine(reason, MessageLevel::Fatal); + emitFailed(tr(reason)); + return; + } + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + { + m_parent->setPid(-1); + emitFailed(tr("Game crashed.")); + return; + } + case LoggedProcess::Finished: + { + m_parent->setPid(-1); + // if the exit code wasn't 0, report this as a crash + auto exitCode = m_process.exitCode(); + if(exitCode != 0) + { + emitFailed(tr("Game crashed.")); + return; + } + //FIXME: make this work again + // m_postlaunchprocess.processEnvironment().insert("INST_EXITCODE", QString(exitCode)); + // run post-exit + emitSucceeded(); + break; + } + case LoggedProcess::Running: + emit logLine(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::Launcher); + m_parent->setPid(m_process.processId()); + m_parent->instance()->setLastLaunch(); + break; + default: + break; + } +} + +void DirectJavaLaunch::setWorkingDirectory(const QString &wd) +{ + m_process.setWorkingDirectory(wd); +} + +void DirectJavaLaunch::proceed() +{ + // nil +} + +bool DirectJavaLaunch::abort() +{ + auto state = m_process.state(); + if (state == LoggedProcess::Running || state == LoggedProcess::Starting) + { + m_process.kill(); + } + return true; +} + diff --git a/ultimmc/launcher/minecraft/launch/DirectJavaLaunch.h b/ultimmc/launcher/minecraft/launch/DirectJavaLaunch.h new file mode 100644 index 0000000..8823017 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/DirectJavaLaunch.h @@ -0,0 +1,58 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "QuickPlayTarget.h" + +class DirectJavaLaunch: public LaunchStep +{ + Q_OBJECT +public: + explicit DirectJavaLaunch(LaunchTask *parent); + virtual ~DirectJavaLaunch() {}; + + virtual void executeTask(); + virtual bool abort(); + virtual void proceed(); + virtual bool canAbort() const + { + return true; + } + void setWorkingDirectory(const QString &wd); + void setAuthSession(AuthSessionPtr session) + { + m_session = session; + } + + void setQuickPlayTarget(QuickPlayTargetPtr quickPlayTarget) + { + m_quickPlayTarget = std::move(quickPlayTarget); + } + +private slots: + void on_state(LoggedProcess::State state); + +private: + LoggedProcess m_process; + QString m_command; + AuthSessionPtr m_session; + QuickPlayTargetPtr m_quickPlayTarget; +}; + diff --git a/ultimmc/launcher/minecraft/launch/ExtractNatives.cpp b/ultimmc/launcher/minecraft/launch/ExtractNatives.cpp new file mode 100644 index 0000000..8cd439b --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/ExtractNatives.cpp @@ -0,0 +1,118 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ExtractNatives.h" +#include +#include + +#include +#include +#include "MMCZip.h" +#include "FileSystem.h" +#include + +#ifdef major + #undef major +#endif +#ifdef minor + #undef minor +#endif + +static QString replaceSuffix (QString target, const QString &suffix, const QString &replacement) +{ + if (!target.endsWith(suffix)) + { + return target; + } + target.resize(target.length() - suffix.length()); + return target + replacement; +} + +static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibHack, bool nativeOpenAL, bool nativeGLFW) +{ + QuaZip zip(source); + if(!zip.open(QuaZip::mdUnzip)) + { + return false; + } + QDir directory(targetFolder); + if (!zip.goToFirstFile()) + { + return false; + } + do + { + QString name = zip.getCurrentFileName(); + auto lowercase = name.toLower(); + if (nativeGLFW && name.contains("glfw")) { + continue; + } + if (nativeOpenAL && name.contains("openal")) { + continue; + } + if(applyJnilibHack) + { + name = replaceSuffix(name, ".jnilib", ".dylib"); + } + QString absFilePath = directory.absoluteFilePath(name); + if (!JlCompress::extractFile(&zip, "", absFilePath)) + { + return false; + } + } while (zip.goToNextFile()); + zip.close(); + if(zip.getZipError()!=0) + { + return false; + } + return true; +} + +void ExtractNatives::executeTask() +{ + auto instance = m_parent->instance(); + std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); + auto toExtract = minecraftInstance->getNativeJars(); + if(toExtract.isEmpty()) + { + emitSucceeded(); + return; + } + auto settings = minecraftInstance->settings(); + bool nativeOpenAL = settings->get("UseNativeOpenAL").toBool(); + bool nativeGLFW = settings->get("UseNativeGLFW").toBool(); + + auto outputPath = minecraftInstance->getNativePath(); + auto javaVersion = minecraftInstance->getJavaVersion(); + bool jniHackEnabled = javaVersion.major() >= 8; + for(const auto &source: toExtract) + { + if(!unzipNatives(source, outputPath, jniHackEnabled, nativeOpenAL, nativeGLFW)) + { + const char *reason = QT_TR_NOOP("Couldn't extract native jar '%1' to destination '%2'"); + emit logLine(QString(reason).arg(source, outputPath), MessageLevel::Fatal); + emitFailed(tr(reason).arg(source, outputPath)); + } + } + emitSucceeded(); +} + +void ExtractNatives::finalize() +{ + auto instance = m_parent->instance(); + QString target_dir = FS::PathCombine(instance->instanceRoot(), "natives/"); + QDir dir(target_dir); + dir.removeRecursively(); +} diff --git a/ultimmc/launcher/minecraft/launch/ExtractNatives.h b/ultimmc/launcher/minecraft/launch/ExtractNatives.h new file mode 100644 index 0000000..094fcd6 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/ExtractNatives.h @@ -0,0 +1,38 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include "minecraft/auth/AuthSession.h" + +// FIXME: temporary wrapper for existing task. +class ExtractNatives: public LaunchStep +{ + Q_OBJECT +public: + explicit ExtractNatives(LaunchTask *parent) : LaunchStep(parent){}; + virtual ~ExtractNatives(){}; + + void executeTask() override; + bool canAbort() const override + { + return false; + } + void finalize() override; +}; + + diff --git a/ultimmc/launcher/minecraft/launch/InjectAuthlib.cpp b/ultimmc/launcher/minecraft/launch/InjectAuthlib.cpp new file mode 100644 index 0000000..09ad983 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/InjectAuthlib.cpp @@ -0,0 +1,158 @@ +#include "InjectAuthlib.h" +#include +#include +#include +#include +#include + +InjectAuthlib::InjectAuthlib(LaunchTask *parent, AuthlibInjectorPtr* injector) : LaunchStep(parent) +{ + m_injector = injector; +} + +void InjectAuthlib::executeTask() +{ + if (m_aborted) + { + emitFailed(tr("Task aborted.")); + return; + } + + auto latestVersionInfo = QString("https://authlib-injector.yushi.moe/artifact/latest.json"); + auto netJob = new NetJob("Injector versions info download", APPLICATION->network()); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("injectors", "version.json"); + if (!m_offlineMode) + { + entry->setStale(true); + auto task = Net::Download::makeCached(QUrl(latestVersionInfo), entry); + netJob->addNetAction(task); + + jobPtr.reset(netJob); + QObject::connect(netJob, &NetJob::succeeded, this, &InjectAuthlib::onVersionDownloadSucceeded); + QObject::connect(netJob, &NetJob::failed, this, &InjectAuthlib::onDownloadFailed); + jobPtr->start(); + } + else + { + onVersionDownloadSucceeded(); + } +} + +void InjectAuthlib::onVersionDownloadSucceeded() +{ + + QByteArray data; + try + { + data = FS::read(QDir("injectors").absoluteFilePath("version.json")); + } + catch (const Exception &e) + { + qCritical() << "Translations Download Failed: index file not readable"; + jobPtr.reset(); + emitFailed("Error while parsing JSON response from InjectorEndpoint"); + return; + } + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(data, &parse_error); + if (parse_error.error != QJsonParseError::NoError) + { + qCritical() << "Error while parsing JSON response from InjectorEndpoint at " << parse_error.offset << " reason: " << parse_error.errorString(); + qCritical() << data; + jobPtr.reset(); + emitFailed("Error while parsing JSON response from InjectorEndpoint"); + return; + } + + if (!doc.isObject()) + { + qCritical() << "Error while parsing JSON response from InjectorEndpoint root is not object"; + qCritical() << data; + jobPtr.reset(); + emitFailed("Error while parsing JSON response from InjectorEndpoint"); + return; + } + + QString downloadUrl; + try + { + downloadUrl = Json::requireString(doc.object(), "download_url"); + } + catch (const JSONValidationError &e) + { + qCritical() << "Error while parsing JSON response from InjectorEndpoint download url is not string"; + qCritical() << e.cause(); + qCritical() << data; + jobPtr.reset(); + emitFailed("Error while parsing JSON response from InjectorEndpoint"); + return; + } + + QFileInfo fi(downloadUrl); + m_versionName = fi.fileName(); + + qDebug() << "Authlib injector version:" << m_versionName; + if (!m_offlineMode) + { + auto netJob = new NetJob("Injector download", APPLICATION->network()); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("injectors", m_versionName); + entry->setStale(true); + auto task = Net::Download::makeCached(QUrl(downloadUrl), entry); + netJob->addNetAction(task); + + jobPtr.reset(netJob); + QObject::connect(netJob, &NetJob::succeeded, this, &InjectAuthlib::onDownloadSucceeded); + QObject::connect(netJob, &NetJob::failed, this, &InjectAuthlib::onDownloadFailed); + jobPtr->start(); + } + else + { + onDownloadSucceeded(); + } +} + +void InjectAuthlib::onDownloadSucceeded() +{ + QString injector = QString("-javaagent:%1=%2").arg(QDir("injectors").absoluteFilePath(m_versionName)).arg(m_authServer); + + qDebug() + << "Injecting " << injector; + auto inj = new AuthlibInjector(injector); + m_injector->reset(inj); + + jobPtr.reset(); + emitSucceeded(); +} + +void InjectAuthlib::onDownloadFailed(QString reason) +{ + jobPtr.reset(); + emitFailed(reason); +} + +void InjectAuthlib::proceed() +{ +} + +bool InjectAuthlib::canAbort() const +{ + if (jobPtr) + { + return jobPtr->canAbort(); + } + return true; +} + +bool InjectAuthlib::abort() +{ + m_aborted = true; + if (jobPtr) + { + if (jobPtr->canAbort()) + { + return jobPtr->abort(); + } + } + return true; +} diff --git a/ultimmc/launcher/minecraft/launch/InjectAuthlib.h b/ultimmc/launcher/minecraft/launch/InjectAuthlib.h new file mode 100644 index 0000000..dd2d085 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/InjectAuthlib.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +struct AuthlibInjector +{ + QString javaArg; + + AuthlibInjector(const QString arg) + { + javaArg = std::move(arg); + qDebug() << "NEW INJECTOR" << javaArg; + } +}; + +typedef std::shared_ptr AuthlibInjectorPtr; + +// FIXME: stupid. should be defined by the instance type? or even completely abstracted away... +class InjectAuthlib : public LaunchStep +{ + Q_OBJECT +public: + InjectAuthlib(LaunchTask *parent, AuthlibInjectorPtr *injector); + virtual ~InjectAuthlib(){}; + + void executeTask() override; + bool canAbort() const override; + void proceed() override; + + void setAuthServer(QString server) + { + m_authServer = server; + }; + + void setOfflineMode(bool offline) { + m_offlineMode = offline; + } + +public slots: + bool abort() override; + +private slots: + void onVersionDownloadSucceeded(); + void onDownloadSucceeded(); + void onDownloadFailed(QString reason); + +private: + shared_qobject_ptr jobPtr; + bool m_aborted = false; + + bool m_offlineMode; + QString m_versionName; + QString m_authServer; + AuthlibInjectorPtr *m_injector; +}; diff --git a/ultimmc/launcher/minecraft/launch/LauncherPartLaunch.cpp b/ultimmc/launcher/minecraft/launch/LauncherPartLaunch.cpp new file mode 100644 index 0000000..1085f94 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/LauncherPartLaunch.cpp @@ -0,0 +1,219 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LauncherPartLaunch.h" + +#include + +#include "launch/LaunchTask.h" +#include "minecraft/MinecraftInstance.h" +#include "FileSystem.h" +#include "Commandline.h" +#include "Application.h" + +LauncherPartLaunch::LauncherPartLaunch(LaunchTask *parent) : LaunchStep(parent) +{ + connect(&m_process, &LoggedProcess::log, this, &LauncherPartLaunch::logLines); + connect(&m_process, &LoggedProcess::stateChanged, this, &LauncherPartLaunch::on_state); +} + +#ifdef Q_OS_WIN +// returns 8.3 file format from long path +#include +QString shortPathName(const QString & file) +{ + auto input = file.toStdWString(); + std::wstring output; + long length = GetShortPathNameW(input.c_str(), NULL, 0); + // NOTE: this resizing might seem weird... + // when GetShortPathNameW fails, it returns length including null character + // when it succeeds, it returns length excluding null character + // See: https://msdn.microsoft.com/en-us/library/windows/desktop/aa364989(v=vs.85).aspx + output.resize(length); + GetShortPathNameW(input.c_str(),(LPWSTR)output.c_str(),length); + output.resize(length-1); + QString ret = QString::fromStdWString(output); + return ret; +} +#endif + +// if the string survives roundtrip through local 8bit encoding... +bool fitsInLocal8bit(const QString & string) +{ + return string == QString::fromLocal8Bit(string.toLocal8Bit()); +} + +void LauncherPartLaunch::executeTask() +{ + auto instance = m_parent->instance(); + std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); + + m_launchScript = minecraftInstance->createLaunchScript(m_session, m_quickPlayTarget); + QStringList args = minecraftInstance->javaArguments(); + QString allArgs = args.join(", "); + emit logLine("Java Arguments:\n[" + m_parent->censorPrivateInfo(allArgs) + "]\n\n", MessageLevel::Launcher); + + auto javaPath = FS::ResolveExecutable(instance->settings()->get("JavaPath").toString()); + + m_process.setProcessEnvironment(instance->createEnvironment()); + + // make detachable - this will keep the process running even if the object is destroyed + m_process.setDetachable(true); + + auto classPath = minecraftInstance->getClassPath(); + classPath.prepend(FS::PathCombine(APPLICATION->getJarsPath(), "NewLaunch.jar")); + + auto natPath = minecraftInstance->getNativePath(); +#ifdef Q_OS_WIN + if (!fitsInLocal8bit(natPath)) + { + args << "-Djava.library.path=" + shortPathName(natPath); + } + else + { + args << "-Djava.library.path=" + natPath; + } +#else + args << "-Djava.library.path=" + natPath; +#endif + + args << "-cp"; +#ifdef Q_OS_WIN + QStringList processed; + for(auto & item: classPath) + { + if (!fitsInLocal8bit(item)) + { + processed << shortPathName(item); + } + else + { + processed << item; + } + } + args << processed.join(';'); +#else + args << classPath.join(':'); +#endif + args << "org.multimc.EntryPoint"; + + qDebug() << args.join(' '); + + QString wrapperCommandStr = instance->getWrapperCommand().trimmed(); + if(!wrapperCommandStr.isEmpty()) + { + auto wrapperArgs = Commandline::splitArgs(wrapperCommandStr); + auto wrapperCommand = wrapperArgs.takeFirst(); + auto realWrapperCommand = QStandardPaths::findExecutable(wrapperCommand); + if (realWrapperCommand.isEmpty()) + { + const char *reason = QT_TR_NOOP("The wrapper command \"%1\" couldn't be found."); + emit logLine(QString(reason).arg(wrapperCommand), MessageLevel::Fatal); + emitFailed(tr(reason).arg(wrapperCommand)); + return; + } + emit logLine("Wrapper command is:\n" + wrapperCommandStr + "\n\n", MessageLevel::Launcher); + args.prepend(javaPath); + m_process.start(wrapperCommand, wrapperArgs + args); + } + else + { + m_process.start(javaPath, args); + } +} + +void LauncherPartLaunch::on_state(LoggedProcess::State state) +{ + switch(state) + { + case LoggedProcess::FailedToStart: + { + //: Error message displayed if instace can't start + const char *reason = QT_TR_NOOP("Could not launch minecraft!"); + emit logLine(reason, MessageLevel::Fatal); + emitFailed(tr(reason)); + return; + } + case LoggedProcess::Aborted: + case LoggedProcess::Crashed: + { + m_parent->setPid(-1); + emitFailed(tr("Game crashed.")); + return; + } + case LoggedProcess::Finished: + { + m_parent->setPid(-1); + // if the exit code wasn't 0, report this as a crash + auto exitCode = m_process.exitCode(); + if(exitCode != 0) + { + emitFailed(tr("Game crashed.")); + return; + } + //FIXME: make this work again + // m_postlaunchprocess.processEnvironment().insert("INST_EXITCODE", QString(exitCode)); + // run post-exit + emitSucceeded(); + break; + } + case LoggedProcess::Running: + emit logLine(QString("Minecraft process ID: %1\n\n").arg(m_process.processId()), MessageLevel::Launcher); + m_parent->setPid(m_process.processId()); + m_parent->instance()->setLastLaunch(); + // send the launch script to the launcher part + m_process.write(m_launchScript.toUtf8()); + + mayProceed = true; + emit readyForLaunch(); + break; + default: + break; + } +} + +void LauncherPartLaunch::setWorkingDirectory(const QString &wd) +{ + m_process.setWorkingDirectory(wd); +} + +void LauncherPartLaunch::proceed() +{ + if(mayProceed) + { + QString launchString("launch\n"); + m_process.write(launchString.toUtf8()); + mayProceed = false; + } +} + +bool LauncherPartLaunch::abort() +{ + if(mayProceed) + { + mayProceed = false; + QString launchString("abort\n"); + m_process.write(launchString.toUtf8()); + } + else + { + auto state = m_process.state(); + if (state == LoggedProcess::Running || state == LoggedProcess::Starting) + { + m_process.kill(); + } + } + return true; +} diff --git a/ultimmc/launcher/minecraft/launch/LauncherPartLaunch.h b/ultimmc/launcher/minecraft/launch/LauncherPartLaunch.h new file mode 100644 index 0000000..c9b0077 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/LauncherPartLaunch.h @@ -0,0 +1,60 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "QuickPlayTarget.h" + +class LauncherPartLaunch: public LaunchStep +{ + Q_OBJECT +public: + explicit LauncherPartLaunch(LaunchTask *parent); + virtual ~LauncherPartLaunch() {}; + + virtual void executeTask(); + virtual bool abort(); + virtual void proceed(); + virtual bool canAbort() const + { + return true; + } + void setWorkingDirectory(const QString &wd); + void setAuthSession(AuthSessionPtr session) + { + m_session = session; + } + + void setQuickPlayTarget(QuickPlayTargetPtr quickPlayTarget) + { + m_quickPlayTarget = std::move(quickPlayTarget); + } + +private slots: + void on_state(LoggedProcess::State state); + +private: + LoggedProcess m_process; + QString m_command; + AuthSessionPtr m_session; + QString m_launchScript; + QuickPlayTargetPtr m_quickPlayTarget; + + bool mayProceed = false; +}; diff --git a/ultimmc/launcher/minecraft/launch/ModMinecraftJar.cpp b/ultimmc/launcher/minecraft/launch/ModMinecraftJar.cpp new file mode 100644 index 0000000..93de9d5 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/ModMinecraftJar.cpp @@ -0,0 +1,82 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModMinecraftJar.h" +#include "launch/LaunchTask.h" +#include "MMCZip.h" +#include "minecraft/OpSys.h" +#include "FileSystem.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +void ModMinecraftJar::executeTask() +{ + auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + + if(!m_inst->getJarMods().size()) + { + emitSucceeded(); + return; + } + // nuke obsolete stripped jar(s) if needed + if(!FS::ensureFolderPathExists(m_inst->binRoot())) + { + emitFailed(tr("Couldn't create the bin folder for Minecraft.jar")); + } + + auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar"); + if(!removeJar()) + { + emitFailed(tr("Couldn't remove stale jar file: %1").arg(finalJarPath)); + } + + // create temporary modded jar, if needed + auto components = m_inst->getPackProfile(); + auto profile = components->getProfile(); + auto jarMods = m_inst->getJarMods(); + if(jarMods.size()) + { + auto mainJar = profile->getMainJar(); + QStringList jars, temp1, temp2, temp3, temp4; + mainJar->getApplicableFiles(currentSystem, jars, temp1, temp2, temp3, m_inst->getLocalLibraryPath()); + auto sourceJarPath = jars[0]; + if(!MMCZip::createModdedJar(sourceJarPath, finalJarPath, jarMods)) + { + emitFailed(tr("Failed to create the custom Minecraft jar file.")); + return; + } + } + emitSucceeded(); +} + +void ModMinecraftJar::finalize() +{ + removeJar(); +} + +bool ModMinecraftJar::removeJar() +{ + auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + auto finalJarPath = QDir(m_inst->binRoot()).absoluteFilePath("minecraft.jar"); + QFile finalJar(finalJarPath); + if(finalJar.exists()) + { + if(!finalJar.remove()) + { + return false; + } + } + return true; +} diff --git a/ultimmc/launcher/minecraft/launch/ModMinecraftJar.h b/ultimmc/launcher/minecraft/launch/ModMinecraftJar.h new file mode 100644 index 0000000..081c6a9 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/ModMinecraftJar.h @@ -0,0 +1,36 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +class ModMinecraftJar: public LaunchStep +{ + Q_OBJECT +public: + explicit ModMinecraftJar(LaunchTask *parent) : LaunchStep(parent) {}; + virtual ~ModMinecraftJar(){}; + + virtual void executeTask() override; + virtual bool canAbort() const override + { + return false; + } + void finalize() override; +private: + bool removeJar(); +}; diff --git a/ultimmc/launcher/minecraft/launch/PrintInstanceInfo.cpp b/ultimmc/launcher/minecraft/launch/PrintInstanceInfo.cpp new file mode 100644 index 0000000..2e6c498 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/PrintInstanceInfo.cpp @@ -0,0 +1,147 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "PrintInstanceInfo.h" +#include + +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) +namespace { +#if defined(Q_OS_LINUX) +void probeProcCpuinfo(QStringList &log) +{ + std::ifstream cpuin("/proc/cpuinfo"); + for (std::string line; std::getline(cpuin, line);) + { + if (strncmp(line.c_str(), "model name", 10) == 0) + { + log << QString::fromStdString(line.substr(13, std::string::npos)); + break; + } + } +} + +void runLspci(QStringList &log) +{ + // FIXME: fixed size buffers... + char buff[512]; + int gpuline = -1; + int cline = 0; + FILE * lspci = popen("lspci -k", "r"); + + if (!lspci) + return; + + while (fgets(buff, 512, lspci) != NULL) + { + std::string str(buff); + if (str.length() < 9) + continue; + if (str.substr(8, 3) == "VGA") + { + gpuline = cline; + log << QString::fromStdString(str.substr(35, std::string::npos)); + } + if (gpuline > -1 && gpuline != cline) + { + if (cline - gpuline < 3) + { + log << QString::fromStdString(str.substr(1, std::string::npos)); + } + } + cline++; + } + pclose(lspci); +} +#elif defined(Q_OS_FREEBSD) +void runSysctlHwModel(QStringList &log) +{ + char buff[512]; + FILE *hwmodel = popen("sysctl hw.model", "r"); + while (fgets(buff, 512, hwmodel) != NULL) + { + log << QString::fromUtf8(buff); + break; + } + pclose(hwmodel); +} + +void runPciconf(QStringList &log) +{ + char buff[512]; + std::string strcard; + FILE *pciconf = popen("pciconf -lv -a vgapci0", "r"); + while (fgets(buff, 512, pciconf) != NULL) + { + if (strncmp(buff, " vendor", 10) == 0) + { + std::string str(buff); + strcard.append(str.substr(str.find_first_of("'") + 1, str.find_last_not_of("'") - (str.find_first_of("'") + 2))); + strcard.append(" "); + } + else if (strncmp(buff, " device", 10) == 0) + { + std::string str2(buff); + strcard.append(str2.substr(str2.find_first_of("'") + 1, str2.find_last_not_of("'") - (str2.find_first_of("'") + 2))); + } + log << QString::fromStdString(strcard); + break; + } + pclose(pciconf); +} +#endif +void runGlxinfo(QStringList & log) +{ + // FIXME: fixed size buffers... + char buff[512]; + FILE *glxinfo = popen("glxinfo", "r"); + if (!glxinfo) + return; + + while (fgets(buff, 512, glxinfo) != NULL) + { + if (strncmp(buff, "OpenGL version string:", 22) == 0) + { + log << QString::fromUtf8(buff); + break; + } + } + pclose(glxinfo); +} + +} +#endif + +void PrintInstanceInfo::executeTask() +{ + auto instance = m_parent->instance(); + QStringList log; + +#if defined(Q_OS_LINUX) + ::probeProcCpuinfo(log); + ::runLspci(log); + ::runGlxinfo(log); +#elif defined(Q_OS_FREEBSD) + ::runSysctlHwModel(log); + ::runPciconf(log); + ::runGlxinfo(log); +#endif + + logLines(log, MessageLevel::Launcher); + logLines(instance->verboseDescription(m_session, m_quickPlayTarget), MessageLevel::Launcher); + emitSucceeded(); +} diff --git a/ultimmc/launcher/minecraft/launch/PrintInstanceInfo.h b/ultimmc/launcher/minecraft/launch/PrintInstanceInfo.h new file mode 100644 index 0000000..600a032 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/PrintInstanceInfo.h @@ -0,0 +1,41 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include "minecraft/auth/AuthSession.h" +#include "minecraft/launch/QuickPlayTarget.h" + +// FIXME: temporary wrapper for existing task. +class PrintInstanceInfo: public LaunchStep +{ + Q_OBJECT +public: + explicit PrintInstanceInfo(LaunchTask *parent, AuthSessionPtr session, QuickPlayTargetPtr quickPlayTarget) : + LaunchStep(parent), m_session(session), m_quickPlayTarget(quickPlayTarget) {}; + virtual ~PrintInstanceInfo(){}; + + virtual void executeTask(); + virtual bool canAbort() const + { + return false; + } +private: + AuthSessionPtr m_session; + QuickPlayTargetPtr m_quickPlayTarget; +}; + diff --git a/ultimmc/launcher/minecraft/launch/QuickPlayTarget.cpp b/ultimmc/launcher/minecraft/launch/QuickPlayTarget.cpp new file mode 100644 index 0000000..b2d2be6 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/QuickPlayTarget.cpp @@ -0,0 +1,74 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "QuickPlayTarget.h" + +#include + +// FIXME: the way this is written, it can't ever do any sort of validation and can accept total junk +QuickPlayTarget QuickPlayTarget::parseMultiplayer(const QString &fullAddress) { + QStringList split = fullAddress.split(":"); + + // The logic below replicates the exact logic minecraft uses for parsing server addresses. + // While the conversion is not lossless and eats errors, it ensures the same behavior + // within Minecraft and MultiMC when entering server addresses. + if (fullAddress.startsWith("[")) + { + int bracket = fullAddress.indexOf("]"); + if (bracket > 0) + { + QString ipv6 = fullAddress.mid(1, bracket - 1); + QString port = fullAddress.mid(bracket + 1).trimmed(); + + if (port.startsWith(":") && !ipv6.isEmpty()) + { + port = port.mid(1); + split = QStringList({ ipv6, port }); + } + else + { + split = QStringList({ipv6}); + } + } + } + + if (split.size() > 2) + { + split = QStringList({fullAddress}); + } + + QString realAddress = split[0]; + + quint16 realPort = 25565; + if (split.size() > 1) + { + bool ok; + realPort = split[1].toUInt(&ok); + + if (!ok) + { + realPort = 25565; + } + } + + return QuickPlayTarget { realAddress, realPort }; +} + +QuickPlayTarget QuickPlayTarget::parseSingleplayer(const QString &worldName) +{ + QuickPlayTarget target; + target.world = worldName; + return target; +} diff --git a/ultimmc/launcher/minecraft/launch/QuickPlayTarget.h b/ultimmc/launcher/minecraft/launch/QuickPlayTarget.h new file mode 100644 index 0000000..4babe82 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/QuickPlayTarget.h @@ -0,0 +1,34 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +struct QuickPlayTarget { + // Multiplayer + QString address; + quint16 port; + + // Singleplayer + QString world; + + static QuickPlayTarget parseMultiplayer(const QString &fullAddress); + static QuickPlayTarget parseSingleplayer(const QString &worldName); +}; + +typedef std::shared_ptr QuickPlayTargetPtr; diff --git a/ultimmc/launcher/minecraft/launch/ReconstructAssets.cpp b/ultimmc/launcher/minecraft/launch/ReconstructAssets.cpp new file mode 100644 index 0000000..4d20666 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/ReconstructAssets.cpp @@ -0,0 +1,36 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ReconstructAssets.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "minecraft/AssetsUtils.h" +#include "launch/LaunchTask.h" + +void ReconstructAssets::executeTask() +{ + auto instance = m_parent->instance(); + std::shared_ptr minecraftInstance = std::dynamic_pointer_cast(instance); + auto components = minecraftInstance->getPackProfile(); + auto profile = components->getProfile(); + auto assets = profile->getMinecraftAssets(); + + if(!AssetsUtils::reconstructAssets(assets->id, minecraftInstance->resourcesDir())) + { + emit logLine("Failed to reconstruct Minecraft assets.", MessageLevel::Error); + } + + emitSucceeded(); +} diff --git a/ultimmc/launcher/minecraft/launch/ReconstructAssets.h b/ultimmc/launcher/minecraft/launch/ReconstructAssets.h new file mode 100644 index 0000000..58d7feb --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/ReconstructAssets.h @@ -0,0 +1,33 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +class ReconstructAssets: public LaunchStep +{ + Q_OBJECT +public: + explicit ReconstructAssets(LaunchTask *parent) : LaunchStep(parent){}; + virtual ~ReconstructAssets(){}; + + void executeTask() override; + bool canAbort() const override + { + return false; + } +}; diff --git a/ultimmc/launcher/minecraft/launch/ScanModFolders.cpp b/ultimmc/launcher/minecraft/launch/ScanModFolders.cpp new file mode 100644 index 0000000..2a0e21b --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/ScanModFolders.cpp @@ -0,0 +1,59 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ScanModFolders.h" +#include "launch/LaunchTask.h" +#include "MMCZip.h" +#include "minecraft/OpSys.h" +#include "FileSystem.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/mod/ModFolderModel.h" + +void ScanModFolders::executeTask() +{ + auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + + auto loaders = m_inst->loaderModList(); + connect(loaders.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::modsDone); + if(!loaders->update()) { + m_modsDone = true; + } + + auto cores = m_inst->coreModList(); + connect(cores.get(), &ModFolderModel::updateFinished, this, &ScanModFolders::coreModsDone); + if(!cores->update()) { + m_coreModsDone = true; + } + checkDone(); +} + +void ScanModFolders::modsDone() +{ + m_modsDone = true; + checkDone(); +} + +void ScanModFolders::coreModsDone() +{ + m_coreModsDone = true; + checkDone(); +} + +void ScanModFolders::checkDone() +{ + if(m_modsDone && m_coreModsDone) { + emitSucceeded(); + } +} diff --git a/ultimmc/launcher/minecraft/launch/ScanModFolders.h b/ultimmc/launcher/minecraft/launch/ScanModFolders.h new file mode 100644 index 0000000..d598917 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/ScanModFolders.h @@ -0,0 +1,42 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +class ScanModFolders: public LaunchStep +{ + Q_OBJECT +public: + explicit ScanModFolders(LaunchTask *parent) : LaunchStep(parent) {}; + virtual ~ScanModFolders(){}; + + virtual void executeTask() override; + virtual bool canAbort() const override + { + return false; + } +private slots: + void coreModsDone(); + void modsDone(); +private: + void checkDone(); + +private: // DATA + bool m_modsDone = false; + bool m_coreModsDone = false; +}; diff --git a/ultimmc/launcher/minecraft/launch/VerifyJavaInstall.cpp b/ultimmc/launcher/minecraft/launch/VerifyJavaInstall.cpp new file mode 100644 index 0000000..e9770aa --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/VerifyJavaInstall.cpp @@ -0,0 +1,75 @@ +/* + * Copyright 2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "VerifyJavaInstall.h" + +#include +#include +#include +#include + +#ifdef major + #undef major +#endif +#ifdef minor + #undef minor +#endif + +void VerifyJavaInstall::executeTask() { + auto m_inst = std::dynamic_pointer_cast(m_parent->instance()); + + auto javaVersion = m_inst->getJavaVersion(); + auto minecraftComponent = m_inst->getPackProfile()->getComponent("net.minecraft"); + + // Java 21 Requirement + if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java21BeginsDate) { + if (javaVersion.major() < 21) { + emit logLine("Minecraft 24w14a and above require the use of Java 21", + MessageLevel::Fatal); + emitFailed(tr("Minecraft 24w14a and above require the use of Java 21")); + return; + } + } + // Java 17 requirement + if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java17BeginsDate) { + if (javaVersion.major() < 17) { + emit logLine("Minecraft 1.18 Pre Release 2 and above require the use of Java 17", + MessageLevel::Fatal); + emitFailed(tr("Minecraft 1.18 Pre Release 2 and above require the use of Java 17")); + return; + } + } + // Java 16 requirement + else if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java16BeginsDate) { + if (javaVersion.major() < 16) { + emit logLine("Minecraft 21w19a and above require the use of Java 16", + MessageLevel::Fatal); + emitFailed(tr("Minecraft 21w19a and above require the use of Java 16")); + return; + } + } + // Java 8 requirement + else if (minecraftComponent->getReleaseDateTime() >= g_VersionFilterData.java8BeginsDate) { + if (javaVersion.major() < 8) { + emit logLine("Minecraft 17w13a and above require the use of Java 8", + MessageLevel::Fatal); + emitFailed(tr("Minecraft 17w13a and above require the use of Java 8")); + return; + } + } + + emitSucceeded(); +} diff --git a/ultimmc/launcher/minecraft/launch/VerifyJavaInstall.h b/ultimmc/launcher/minecraft/launch/VerifyJavaInstall.h new file mode 100644 index 0000000..dfcb991 --- /dev/null +++ b/ultimmc/launcher/minecraft/launch/VerifyJavaInstall.h @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +class VerifyJavaInstall : public LaunchStep { + Q_OBJECT + +public: + explicit VerifyJavaInstall(LaunchTask *parent) : LaunchStep(parent) { + }; + ~VerifyJavaInstall() override = default; + + void executeTask() override; + bool canAbort() const override { + return false; + } +}; diff --git a/ultimmc/launcher/minecraft/legacy/LegacyInstance.cpp b/ultimmc/launcher/minecraft/legacy/LegacyInstance.cpp new file mode 100644 index 0000000..3ea9e78 --- /dev/null +++ b/ultimmc/launcher/minecraft/legacy/LegacyInstance.cpp @@ -0,0 +1,256 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include "LegacyInstance.h" + +#include "minecraft/legacy/LegacyModList.h" +#include "minecraft/WorldList.h" +#include +#include + +LegacyInstance::LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir) + : BaseInstance(globalSettings, settings, rootDir) +{ + settings->registerSetting("NeedsRebuild", true); + settings->registerSetting("ShouldUpdate", false); + settings->registerSetting("JarVersion", QString()); + settings->registerSetting("IntendedJarVersion", QString()); + /* + * custom base jar has no default. it is determined in code... see the accessor methods for + *it + * + * for instances that DO NOT have the CustomBaseJar setting (legacy instances), + * [.]minecraft/bin/mcbackup.jar is the default base jar + */ + settings->registerSetting("UseCustomBaseJar", true); + settings->registerSetting("CustomBaseJar", ""); +} + +QString LegacyInstance::mainJarToPreserve() const +{ + bool customJar = m_settings->get("UseCustomBaseJar").toBool(); + if(customJar) + { + auto base = baseJar(); + if(QFile::exists(base)) + { + return base; + } + } + auto runnable = runnableJar(); + if(QFile::exists(runnable)) + { + return runnable; + } + return QString(); +} + + +QString LegacyInstance::baseJar() const +{ + bool customJar = m_settings->get("UseCustomBaseJar").toBool(); + if (customJar) + { + return customBaseJar(); + } + else + return defaultBaseJar(); +} + +QString LegacyInstance::customBaseJar() const +{ + QString value = m_settings->get("CustomBaseJar").toString(); + if (value.isNull() || value.isEmpty()) + { + return defaultCustomBaseJar(); + } + return value; +} + +bool LegacyInstance::shouldUseCustomBaseJar() const +{ + return m_settings->get("UseCustomBaseJar").toBool(); +} + + +Task::Ptr LegacyInstance::createUpdateTask(Net::Mode) +{ + return nullptr; +} + +std::shared_ptr LegacyInstance::jarModList() const +{ + if (!jar_mod_list) + { + auto list = new LegacyModList(jarModsDir(), modListFile()); + jar_mod_list.reset(list); + } + jar_mod_list->update(); + return jar_mod_list; +} + +QString LegacyInstance::gameRoot() const +{ + QFileInfo mcDir(FS::PathCombine(instanceRoot(), "minecraft")); + QFileInfo dotMCDir(FS::PathCombine(instanceRoot(), ".minecraft")); + + if (mcDir.exists() && !dotMCDir.exists()) + return mcDir.filePath(); + else + return dotMCDir.filePath(); +} + +QString LegacyInstance::binRoot() const +{ + return FS::PathCombine(gameRoot(), "bin"); +} + +QString LegacyInstance::modsRoot() const { + return FS::PathCombine(gameRoot(), "mods"); +} + + +QString LegacyInstance::jarModsDir() const +{ + return FS::PathCombine(instanceRoot(), "instMods"); +} + +QString LegacyInstance::libDir() const +{ + return FS::PathCombine(gameRoot(), "lib"); +} + +QString LegacyInstance::savesDir() const +{ + return FS::PathCombine(gameRoot(), "saves"); +} + +QString LegacyInstance::coreModsDir() const +{ + return FS::PathCombine(gameRoot(), "coremods"); +} + +QString LegacyInstance::resourceDir() const +{ + return FS::PathCombine(gameRoot(), "resources"); +} +QString LegacyInstance::texturePacksDir() const +{ + return FS::PathCombine(gameRoot(), "texturepacks"); +} + +QString LegacyInstance::runnableJar() const +{ + return FS::PathCombine(binRoot(), "minecraft.jar"); +} + +QString LegacyInstance::modListFile() const +{ + return FS::PathCombine(instanceRoot(), "modlist"); +} + +QString LegacyInstance::instanceConfigFolder() const +{ + return FS::PathCombine(gameRoot(), "config"); +} + +bool LegacyInstance::shouldRebuild() const +{ + return m_settings->get("NeedsRebuild").toBool(); +} + +QString LegacyInstance::currentVersionId() const +{ + return m_settings->get("JarVersion").toString(); +} + +QString LegacyInstance::intendedVersionId() const +{ + return m_settings->get("IntendedJarVersion").toString(); +} + +bool LegacyInstance::shouldUpdate() const +{ + QVariant var = settings()->get("ShouldUpdate"); + if (!var.isValid() || var.toBool() == false) + { + return intendedVersionId() != currentVersionId(); + } + return true; +} + +QString LegacyInstance::defaultBaseJar() const +{ + return "versions/" + intendedVersionId() + "/" + intendedVersionId() + ".jar"; +} + +QString LegacyInstance::defaultCustomBaseJar() const +{ + return FS::PathCombine(binRoot(), "mcbackup.jar"); +} + +std::shared_ptr LegacyInstance::worldList() const +{ + if (!m_world_list) + { + m_world_list.reset(new WorldList(savesDir())); + } + return m_world_list; +} + +QString LegacyInstance::typeName() const +{ + return tr("Legacy"); +} + +QString LegacyInstance::getStatusbarDescription() +{ + return tr("Instance from previous versions."); +} + +QStringList LegacyInstance::verboseDescription(AuthSessionPtr session, QuickPlayTargetPtr quickPlayTarget) +{ + QStringList out; + + auto alltraits = traits(); + if(alltraits.size()) + { + out << "Traits:"; + for (auto trait : alltraits) + { + out << " " + trait; + } + out << ""; + } + + QString windowParams; + if (settings()->get("LaunchMaximized").toBool()) + { + out << "Window size: max (if available)"; + } + else + { + auto width = settings()->get("MinecraftWinWidth").toInt(); + auto height = settings()->get("MinecraftWinHeight").toInt(); + out << "Window size: " + QString::number(width) + " x " + QString::number(height); + } + out << ""; + return out; +} diff --git a/ultimmc/launcher/minecraft/legacy/LegacyInstance.h b/ultimmc/launcher/minecraft/legacy/LegacyInstance.h new file mode 100644 index 0000000..f37ecb0 --- /dev/null +++ b/ultimmc/launcher/minecraft/legacy/LegacyInstance.h @@ -0,0 +1,142 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "BaseInstance.h" +#include "launch/LaunchTask.h" + +class ModFolderModel; +class LegacyModList; +class WorldList; +class Task; +/* + * WHY: Legacy instances - from MultiMC 3 and 4 - are here only to provide a way to upgrade them to the current format. + */ +class LegacyInstance : public BaseInstance +{ + Q_OBJECT +public: + + explicit LegacyInstance(SettingsObjectPtr globalSettings, SettingsObjectPtr settings, const QString &rootDir); + + virtual void saveNow() override {} + + /// Path to the instance's minecraft.jar + QString runnableJar() const; + + //! Path to the instance's modlist file. + QString modListFile() const; + + ////// Directories ////// + QString libDir() const; + QString savesDir() const; + QString texturePacksDir() const; + QString jarModsDir() const; + QString coreModsDir() const; + QString resourceDir() const; + + QString instanceConfigFolder() const override; + + QString gameRoot() const override; // Path to the instance's minecraft directory. + QString modsRoot() const override; // Path to the instance's minecraft directory. + QString binRoot() const; // Path to the instance's minecraft bin directory. + + /// Get the curent base jar of this instance. By default, it's the + /// versions/$version/$version.jar + QString baseJar() const; + + /// the default base jar of this instance + QString defaultBaseJar() const; + /// the default custom base jar of this instance + QString defaultCustomBaseJar() const; + + // the main jar that we actually want to keep when migrating the instance + QString mainJarToPreserve() const; + + /*! + * Whether or not custom base jar is used + */ + bool shouldUseCustomBaseJar() const; + + /*! + * The value of the custom base jar + */ + QString customBaseJar() const; + + std::shared_ptr jarModList() const; + std::shared_ptr worldList() const; + + /*! + * Whether or not the instance's minecraft.jar needs to be rebuilt. + * If this is true, when the instance launches, its jar mods will be + * re-added to a fresh minecraft.jar file. + */ + bool shouldRebuild() const; + + QString currentVersionId() const; + QString intendedVersionId() const; + + QSet traits() const override + { + return {"legacy-instance", "texturepacks"}; + }; + + virtual bool shouldUpdate() const; + virtual Task::Ptr createUpdateTask(Net::Mode mode) override; + + virtual QString typeName() const override; + + bool canLaunch() const override + { + return false; + } + bool canEdit() const override + { + return true; + } + bool canExport() const override + { + return false; + } + shared_qobject_ptr createLaunchTask( + AuthSessionPtr account, QuickPlayTargetPtr quickPlayTarget, quint16 localAuthServerPort) override + { + return nullptr; + } + IPathMatcher::Ptr getLogFileMatcher() override + { + return nullptr; + } + QString getLogFileRoot() override + { + return gameRoot(); + } + + QString getStatusbarDescription() override; + QStringList verboseDescription(AuthSessionPtr session, QuickPlayTargetPtr quickPlayTarget) override; + + QProcessEnvironment createEnvironment() override + { + return QProcessEnvironment(); + } + QMap getVariables() const override + { + return {}; + } +protected: + mutable std::shared_ptr jar_mod_list; + mutable std::shared_ptr m_world_list; +}; diff --git a/ultimmc/launcher/minecraft/legacy/LegacyModList.cpp b/ultimmc/launcher/minecraft/legacy/LegacyModList.cpp new file mode 100644 index 0000000..e9948ab --- /dev/null +++ b/ultimmc/launcher/minecraft/legacy/LegacyModList.cpp @@ -0,0 +1,136 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LegacyModList.h" +#include +#include +#include + +LegacyModList::LegacyModList(const QString &dir, const QString &list_file) + : m_dir(dir), m_list_file(list_file) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); +} + + struct OrderItem + { + QString id; + bool enabled = false; + }; + typedef QList OrderList; + +static void internalSort(QList &what) +{ + auto predicate = [](const LegacyModList::Mod &left, const LegacyModList::Mod &right) + { + return left.fileName().localeAwareCompare(right.fileName()) < 0; + }; + std::sort(what.begin(), what.end(), predicate); +} + +static OrderList readListFile(const QString &m_list_file) +{ + OrderList itemList; + if (m_list_file.isNull() || m_list_file.isEmpty()) + return itemList; + + QFile textFile(m_list_file); + if (!textFile.open(QIODevice::ReadOnly | QIODevice::Text)) + return OrderList(); + + QTextStream textStream; + textStream.setAutoDetectUnicode(true); + textStream.setDevice(&textFile); + while (true) + { + QString line = textStream.readLine(); + if (line.isNull() || line.isEmpty()) + break; + else + { + OrderItem it; + it.enabled = !line.endsWith(".disabled"); + if (!it.enabled) + { + line.chop(9); + } + it.id = line; + itemList.append(it); + } + } + textFile.close(); + return itemList; +} + +bool LegacyModList::update() +{ + if (!m_dir.exists() || !m_dir.isReadable()) + return false; + + QList orderedMods; + QList newMods; + m_dir.refresh(); + auto folderContents = m_dir.entryInfoList(); + + // first, process the ordered items (if any) + OrderList listOrder = readListFile(m_list_file); + for (auto item : listOrder) + { + QFileInfo infoEnabled(m_dir.filePath(item.id)); + QFileInfo infoDisabled(m_dir.filePath(item.id + ".disabled")); + int idxEnabled = folderContents.indexOf(infoEnabled); + int idxDisabled = folderContents.indexOf(infoDisabled); + bool isEnabled; + // if both enabled and disabled versions are present, it's a special case... + if (idxEnabled >= 0 && idxDisabled >= 0) + { + // we only process the one we actually have in the order file. + // and exactly as we have it. + // THIS IS A CORNER CASE + isEnabled = item.enabled; + } + else + { + // only one is present. + // we pick the one that we found. + // we assume the mod was enabled/disabled by external means + isEnabled = idxEnabled >= 0; + } + int idx = isEnabled ? idxEnabled : idxDisabled; + QFileInfo &info = isEnabled ? infoEnabled : infoDisabled; + // if the file from the index file exists + if (idx != -1) + { + // remove from the actual folder contents list + folderContents.takeAt(idx); + // append the new mod + orderedMods.append(info); + } + } + // if there are any untracked files... append them sorted at the end + if (folderContents.size()) + { + for (auto entry : folderContents) + { + newMods.append(entry); + } + internalSort(newMods); + orderedMods.append(newMods); + } + mods.swap(orderedMods); + return true; +} diff --git a/ultimmc/launcher/minecraft/legacy/LegacyModList.h b/ultimmc/launcher/minecraft/legacy/LegacyModList.h new file mode 100644 index 0000000..fade736 --- /dev/null +++ b/ultimmc/launcher/minecraft/legacy/LegacyModList.h @@ -0,0 +1,47 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +class LegacyModList +{ +public: + + using Mod = QFileInfo; + + LegacyModList(const QString &dir, const QString &list_file = QString()); + + /// Reloads the mod list and returns true if the list changed. + bool update(); + + QDir dir() + { + return m_dir; + } + + const QList & allMods() + { + return mods; + } + +protected: + QDir m_dir; + QString m_list_file; + QList mods; +}; diff --git a/ultimmc/launcher/minecraft/legacy/LegacyUpgradeTask.cpp b/ultimmc/launcher/minecraft/legacy/LegacyUpgradeTask.cpp new file mode 100644 index 0000000..a4ea60c --- /dev/null +++ b/ultimmc/launcher/minecraft/legacy/LegacyUpgradeTask.cpp @@ -0,0 +1,138 @@ +#include "LegacyUpgradeTask.h" +#include "settings/INISettingsObject.h" +#include "FileSystem.h" +#include "NullInstance.h" +#include "pathmatcher/RegexpMatcher.h" +#include +#include "LegacyInstance.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "LegacyModList.h" +#include "classparser.h" + +LegacyUpgradeTask::LegacyUpgradeTask(InstancePtr origInstance) +{ + m_origInstance = origInstance; +} + +void LegacyUpgradeTask::executeTask() +{ + setStatus(tr("Copying instance %1").arg(m_origInstance->name())); + + FS::copy folderCopy(m_origInstance->instanceRoot(), m_stagingPath); + folderCopy.followSymlinks(true); + + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), folderCopy); + connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &LegacyUpgradeTask::copyFinished); + connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &LegacyUpgradeTask::copyAborted); + m_copyFutureWatcher.setFuture(m_copyFuture); +} + +static QString decideVersion(const QString& currentVersion, const QString& intendedVersion) +{ + if(intendedVersion != currentVersion) + { + if(!intendedVersion.isEmpty()) + { + return intendedVersion; + } + else if(!currentVersion.isEmpty()) + { + return currentVersion; + } + } + else + { + if(!intendedVersion.isEmpty()) + { + return intendedVersion; + } + } + return QString(); +} + +void LegacyUpgradeTask::copyFinished() +{ + auto successful = m_copyFuture.result(); + if(!successful) + { + emitFailed(tr("Instance folder copy failed.")); + return; + } + auto legacyInst = std::dynamic_pointer_cast(m_origInstance); + + auto instanceSettings = std::make_shared(FS::PathCombine(m_stagingPath, "instance.cfg")); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + // NOTE: this scope ensures the instance is fully saved before we emitSucceeded + { + MinecraftInstance inst(m_globalSettings, instanceSettings, m_stagingPath); + inst.setName(m_instName); + + QString preferredVersionNumber = decideVersion(legacyInst->currentVersionId(), legacyInst->intendedVersionId()); + if(preferredVersionNumber.isNull()) + { + // try to decide version based on the jar(s?) + preferredVersionNumber = classparser::GetMinecraftJarVersion(legacyInst->baseJar()); + if(preferredVersionNumber.isNull()) + { + preferredVersionNumber = classparser::GetMinecraftJarVersion(legacyInst->runnableJar()); + if(preferredVersionNumber.isNull()) + { + emitFailed(tr("Could not decide Minecraft version.")); + return; + } + } + } + auto components = inst.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", preferredVersionNumber, true); + + QString jarPath = legacyInst->mainJarToPreserve(); + if(!jarPath.isNull()) + { + qDebug() << "Preserving base jar! : " << jarPath; + // FIXME: handle case when the jar is unreadable? + // TODO: check the hash, if it's the same as the upstream jar, do not do this + components->installCustomJar(jarPath); + } + + auto jarMods = legacyInst->jarModList()->allMods(); + for(auto & jarMod: jarMods) + { + QString modPath = jarMod.absoluteFilePath(); + qDebug() << "jarMod: " << modPath; + components->installJarMods({modPath}); + } + + // remove all the extra garbage we no longer need + auto removeAll = [&](const QString &root, const QStringList &things) + { + for(auto &thing : things) + { + auto removePath = FS::PathCombine(root, thing); + QFileInfo stat(removePath); + if(stat.isDir()) + { + FS::deletePath(removePath); + } + else + { + QFile::remove(removePath); + } + } + }; + QStringList rootRemovables = {"modlist", "version", "instMods"}; + QStringList mcRemovables = {"bin", "MultiMCLauncher.jar", "icon.png"}; + removeAll(inst.instanceRoot(), rootRemovables); + removeAll(inst.gameRoot(), mcRemovables); + } + emitSucceeded(); +} + +void LegacyUpgradeTask::copyAborted() +{ + emitFailed(tr("Instance folder copy has been aborted.")); + return; +} + diff --git a/ultimmc/launcher/minecraft/legacy/LegacyUpgradeTask.h b/ultimmc/launcher/minecraft/legacy/LegacyUpgradeTask.h new file mode 100644 index 0000000..542e17b --- /dev/null +++ b/ultimmc/launcher/minecraft/legacy/LegacyUpgradeTask.h @@ -0,0 +1,29 @@ +#pragma once + +#include "InstanceTask.h" +#include "net/NetJob.h" +#include +#include +#include +#include "settings/SettingsObject.h" +#include "BaseVersion.h" +#include "BaseInstance.h" + + +class LegacyUpgradeTask : public InstanceTask +{ + Q_OBJECT +public: + explicit LegacyUpgradeTask(InstancePtr origInstance); + +protected: + //! Entry point for tasks. + virtual void executeTask() override; + void copyFinished(); + void copyAborted(); + +private: /* data */ + InstancePtr m_origInstance; + QFuture m_copyFuture; + QFutureWatcher m_copyFutureWatcher; +}; diff --git a/ultimmc/launcher/minecraft/mod/LocalModParseTask.cpp b/ultimmc/launcher/minecraft/mod/LocalModParseTask.cpp new file mode 100644 index 0000000..eb101b1 --- /dev/null +++ b/ultimmc/launcher/minecraft/mod/LocalModParseTask.cpp @@ -0,0 +1,537 @@ +#include "LocalModParseTask.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "settings/INIFile.h" +#include "FileSystem.h" + +namespace { + +// NEW format +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/6f62b37cea040daf350dc253eae6326dd9c822c3 + +// OLD format: +// https://github.com/MinecraftForge/FML/wiki/FML-mod-information-file/5bf6a2d05145ec79387acc0d45c958642fb049fc +std::shared_ptr ReadMCModInfo(QByteArray contents) +{ + auto getInfoFromArray = [&](QJsonArray arr)->std::shared_ptr + { + if (!arr.at(0).isObject()) { + return nullptr; + } + std::shared_ptr details = std::make_shared(); + auto firstObj = arr.at(0).toObject(); + details->mod_id = firstObj.value("modid").toString(); + auto name = firstObj.value("name").toString(); + // NOTE: ignore stupid example mods copies where the author didn't even bother to change the name + if(name != "Example Mod") { + details->name = name; + } + details->version = firstObj.value("version").toString(); + details->updateurl = firstObj.value("updateUrl").toString(); + auto homeurl = firstObj.value("url").toString().trimmed(); + if(!homeurl.isEmpty()) + { + // fix up url. + if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) + { + homeurl.prepend("http://"); + } + } + details->homeurl = homeurl; + details->description = firstObj.value("description").toString(); + QJsonArray authors = firstObj.value("authorList").toArray(); + if (authors.size() == 0) { + // FIXME: what is the format of this? is there any? + authors = firstObj.value("authors").toArray(); + } + + for (auto author: authors) + { + details->authors.append(author.toString()); + } + details->credits = firstObj.value("credits").toString(); + return details; + }; + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + // this is the very old format that had just the array + if (jsonDoc.isArray()) + { + return getInfoFromArray(jsonDoc.array()); + } + else if (jsonDoc.isObject()) + { + auto val = jsonDoc.object().value("modinfoversion"); + if(val.isUndefined()) { + val = jsonDoc.object().value("modListVersion"); + } + int version = val.toDouble(); + if (version != 2) + { + qCritical() << "BAD stuff happened to mod json:"; + qCritical() << contents; + return nullptr; + } + auto arrVal = jsonDoc.object().value("modlist"); + if(arrVal.isUndefined()) { + arrVal = jsonDoc.object().value("modList"); + } + if (arrVal.isArray()) + { + return getInfoFromArray(arrVal.toArray()); + } + } + return nullptr; +} + +// https://github.com/MinecraftForge/Documentation/blob/5ab4ba6cf9abc0ac4c0abd96ad187461aefd72af/docs/gettingstarted/structuring.md +std::shared_ptr ReadMCModTOML(QByteArray contents) +{ + std::shared_ptr details = std::make_shared(); + + char errbuf[200]; + // top-level table + toml_table_t* tomlData = toml_parse(contents.data(), errbuf, sizeof(errbuf)); + + if(!tomlData) + { + return nullptr; + } + + // array defined by [[mods]] + toml_array_t* tomlModsArr = toml_array_in(tomlData, "mods"); + if(!tomlModsArr) + { + qWarning() << "Corrupted mods.toml? Couldn't find [[mods]] array!"; + return nullptr; + } + + // we only really care about the first element, since multiple mods in one file is not supported by us at the moment + toml_table_t* tomlModsTable0 = toml_table_at(tomlModsArr, 0); + if(!tomlModsTable0) + { + qWarning() << "Corrupted mods.toml? [[mods]] didn't have an element at index 0!"; + return nullptr; + } + + // mandatory properties - always in [[mods]] + toml_datum_t modIdDatum = toml_string_in(tomlModsTable0, "modId"); + if(modIdDatum.ok) + { + details->mod_id = modIdDatum.u.s; + // library says this is required for strings + free(modIdDatum.u.s); + } + toml_datum_t versionDatum = toml_string_in(tomlModsTable0, "version"); + if(versionDatum.ok) + { + details->version = versionDatum.u.s; + free(versionDatum.u.s); + } + toml_datum_t displayNameDatum = toml_string_in(tomlModsTable0, "displayName"); + if(displayNameDatum.ok) + { + details->name = displayNameDatum.u.s; + free(displayNameDatum.u.s); + } + toml_datum_t descriptionDatum = toml_string_in(tomlModsTable0, "description"); + if(descriptionDatum.ok) + { + details->description = descriptionDatum.u.s; + free(descriptionDatum.u.s); + } + + // optional properties - can be in the root table or [[mods]] + toml_datum_t authorsDatum = toml_string_in(tomlData, "authors"); + QString authors = ""; + if(authorsDatum.ok) + { + authors = authorsDatum.u.s; + free(authorsDatum.u.s); + } + else + { + authorsDatum = toml_string_in(tomlModsTable0, "authors"); + if(authorsDatum.ok) + { + authors = authorsDatum.u.s; + free(authorsDatum.u.s); + } + } + if(!authors.isEmpty()) + { + // author information is stored as a string now, not a list + details->authors.append(authors); + } + // is credits even used anywhere? including this for completion/parity with old data version + toml_datum_t creditsDatum = toml_string_in(tomlData, "credits"); + QString credits = ""; + if(creditsDatum.ok) + { + authors = creditsDatum.u.s; + free(creditsDatum.u.s); + } + else + { + creditsDatum = toml_string_in(tomlModsTable0, "credits"); + if(creditsDatum.ok) + { + credits = creditsDatum.u.s; + free(creditsDatum.u.s); + } + } + details->credits = credits; + toml_datum_t homeurlDatum = toml_string_in(tomlData, "displayURL"); + QString homeurl = ""; + if(homeurlDatum.ok) + { + homeurl = homeurlDatum.u.s; + free(homeurlDatum.u.s); + } + else + { + homeurlDatum = toml_string_in(tomlModsTable0, "displayURL"); + if(homeurlDatum.ok) + { + homeurl = homeurlDatum.u.s; + free(homeurlDatum.u.s); + } + } + if(!homeurl.isEmpty()) + { + // fix up url. + if (!homeurl.startsWith("http://") && !homeurl.startsWith("https://") && !homeurl.startsWith("ftp://")) + { + homeurl.prepend("http://"); + } + } + details->homeurl = homeurl; + + // this seems to be recursive, so it should free everything + toml_free(tomlData); + + return details; +} + +// https://fabricmc.net/wiki/documentation:fabric_mod_json +std::shared_ptr ReadFabricModInfo(QByteArray contents) +{ + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + auto schemaVersion = object.contains("schemaVersion") ? object.value("schemaVersion").toInt(0) : 0; + + std::shared_ptr details = std::make_shared(); + + details->mod_id = object.value("id").toString(); + details->version = object.value("version").toString(); + + details->name = object.contains("name") ? object.value("name").toString() : details->mod_id; + details->description = object.value("description").toString(); + + if (schemaVersion >= 1) + { + QJsonArray authors = object.value("authors").toArray(); + for (auto author: authors) + { + if(author.isObject()) { + details->authors.append(author.toObject().value("name").toString()); + } + else { + details->authors.append(author.toString()); + } + } + + if (object.contains("contact")) + { + QJsonObject contact = object.value("contact").toObject(); + + if (contact.contains("homepage")) + { + details->homeurl = contact.value("homepage").toString(); + } + } + } + return details; +} + +// https://github.com/QuiltMC/rfcs/blob/master/specification/0002-quilt.mod.json.md +std::shared_ptr ReadQuiltModInfo(QByteArray contents) +{ + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + + std::shared_ptr details = std::make_shared(); + + if (object.contains("schema_version") && object.value("schema_version").toInt() == 1) + { + QJsonObject loader = object.value("quilt_loader").toObject(); + details->mod_id = loader.value("id").toString(); + details->version = loader.value("version").toString(); + + if (loader.contains("metadata")) + { + QJsonObject metadata = loader.value("metadata").toObject(); + details->name = metadata.contains("name") ? metadata.value("name").toString() : details->mod_id; + + if (metadata.contains("description")) + { + details->description = metadata.value("description").toString(); + } + + if (metadata.contains("contributors")) + { + // NOTE: This lists every contributor, not just "authors" + details->authors = metadata.value("contributors").toObject().keys(); + } + + if (object.contains("contact")) + { + QJsonObject contact = object.value("contact").toObject(); + + if (contact.contains("homepage")) + { + details->homeurl = contact.value("homepage").toString(); + } + } + } + } + return details; +} + +std::shared_ptr ReadForgeInfo(QByteArray contents) +{ + std::shared_ptr details = std::make_shared(); + // Read the data + details->name = "Minecraft Forge"; + details->mod_id = "Forge"; + details->homeurl = "http://www.minecraftforge.net/forum/"; + INIFile ini; + if (!ini.loadFile(contents)) + return details; + + QString major = ini.get("forge.major.number", "0").toString(); + QString minor = ini.get("forge.minor.number", "0").toString(); + QString revision = ini.get("forge.revision.number", "0").toString(); + QString build = ini.get("forge.build.number", "0").toString(); + + details->version = major + "." + minor + "." + revision + "." + build; + return details; +} + +std::shared_ptr ReadLiteModInfo(QByteArray contents) +{ + std::shared_ptr details = std::make_shared(); + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(contents, &jsonError); + auto object = jsonDoc.object(); + if (object.contains("name")) + { + details->mod_id = details->name = object.value("name").toString(); + } + if (object.contains("version")) + { + details->version = object.value("version").toString(""); + } + else + { + details->version = object.value("revision").toString(""); + } + details->mcversion = object.value("mcversion").toString(); + auto author = object.value("author").toString(); + if(!author.isEmpty()) { + details->authors.append(author); + } + details->description = object.value("description").toString(); + details->homeurl = object.value("url").toString(); + return details; +} + +} + +LocalModParseTask::LocalModParseTask(int token, Mod::ModType type, const QFileInfo& modFile): + m_token(token), + m_type(type), + m_modFile(modFile), + m_result(new Result()) +{ +} + +void LocalModParseTask::processAsZip() +{ + QuaZip zip(m_modFile.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("META-INF/mods.toml")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadMCModTOML(file.readAll()); + file.close(); + + // to replace ${file.jarVersion} with the actual version, as needed + if (m_result->details && m_result->details->version == "${file.jarVersion}") + { + if (zip.setCurrentFile("META-INF/MANIFEST.MF")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + // quick and dirty line-by-line parser + auto manifestLines = file.readAll().split('\n'); + QString manifestVersion = ""; + for (auto &line : manifestLines) + { + if (QString(line).startsWith("Implementation-Version: ")) + { + manifestVersion = QString(line).remove("Implementation-Version: "); + break; + } + } + + // some mods use ${projectversion} in their build.gradle, causing this mess to show up in MANIFEST.MF + // also keep with forge's behavior of setting the version to "NONE" if none is found + if (manifestVersion.contains("task ':jar' property 'archiveVersion'") || manifestVersion == "") + { + manifestVersion = "NONE"; + } + + m_result->details->version = manifestVersion; + + file.close(); + } + } + + zip.close(); + return; + } + else if (zip.setCurrentFile("mcmod.info")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadMCModInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + else if (zip.setCurrentFile("fabric.mod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadFabricModInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + else if (zip.setCurrentFile("quilt.mod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadQuiltModInfo(file.readAll()); + file.close(); + zip.close(); + return; + + } + else if (zip.setCurrentFile("forgeversion.properties")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadForgeInfo(file.readAll()); + file.close(); + zip.close(); + return; + } + + zip.close(); +} + +void LocalModParseTask::processAsFolder() +{ + QFileInfo mcmod_info(FS::PathCombine(m_modFile.filePath(), "mcmod.info")); + if (mcmod_info.isFile()) + { + QFile mcmod(mcmod_info.filePath()); + if (!mcmod.open(QIODevice::ReadOnly)) + return; + auto data = mcmod.readAll(); + if (data.isEmpty() || data.isNull()) + return; + m_result->details = ReadMCModInfo(data); + } +} + +void LocalModParseTask::processAsLitemod() +{ + QuaZip zip(m_modFile.filePath()); + if (!zip.open(QuaZip::mdUnzip)) + return; + + QuaZipFile file(&zip); + + if (zip.setCurrentFile("litemod.json")) + { + if (!file.open(QIODevice::ReadOnly)) + { + zip.close(); + return; + } + + m_result->details = ReadLiteModInfo(file.readAll()); + file.close(); + } + zip.close(); +} + +void LocalModParseTask::run() +{ + switch(m_type) + { + case Mod::MOD_ZIPFILE: + processAsZip(); + break; + case Mod::MOD_FOLDER: + processAsFolder(); + break; + case Mod::MOD_LITEMOD: + processAsLitemod(); + break; + default: + break; + } + emit finished(m_token); +} diff --git a/ultimmc/launcher/minecraft/mod/LocalModParseTask.h b/ultimmc/launcher/minecraft/mod/LocalModParseTask.h new file mode 100644 index 0000000..0f119ba --- /dev/null +++ b/ultimmc/launcher/minecraft/mod/LocalModParseTask.h @@ -0,0 +1,37 @@ +#pragma once +#include +#include +#include +#include "Mod.h" +#include "ModDetails.h" + +class LocalModParseTask : public QObject, public QRunnable +{ + Q_OBJECT +public: + struct Result { + QString id; + std::shared_ptr details; + }; + using ResultPtr = std::shared_ptr; + ResultPtr result() const { + return m_result; + } + + LocalModParseTask(int token, Mod::ModType type, const QFileInfo & modFile); + void run(); + +signals: + void finished(int token); + +private: + void processAsZip(); + void processAsFolder(); + void processAsLitemod(); + +private: + int m_token; + Mod::ModType m_type; + QFileInfo m_modFile; + ResultPtr m_result; +}; diff --git a/ultimmc/launcher/minecraft/mod/Mod.cpp b/ultimmc/launcher/minecraft/mod/Mod.cpp new file mode 100644 index 0000000..b6bff29 --- /dev/null +++ b/ultimmc/launcher/minecraft/mod/Mod.cpp @@ -0,0 +1,151 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "Mod.h" +#include +#include + +namespace { + +ModDetails invalidDetails; + +} + + +Mod::Mod(const QFileInfo &file) +{ + repath(file); + m_changedDateTime = file.lastModified(); +} + +void Mod::repath(const QFileInfo &file) +{ + m_file = file; + QString name_base = file.fileName(); + + m_type = Mod::MOD_UNKNOWN; + + m_mmc_id = name_base; + + if (m_file.isDir()) + { + m_type = MOD_FOLDER; + m_name = name_base; + } + else if (m_file.isFile()) + { + if (name_base.endsWith(".disabled")) + { + m_enabled = false; + name_base.chop(9); + } + else + { + m_enabled = true; + } + if (name_base.endsWith(".zip") || name_base.endsWith(".jar")) + { + m_type = MOD_ZIPFILE; + name_base.chop(4); + } + else if (name_base.endsWith(".litemod")) + { + m_type = MOD_LITEMOD; + name_base.chop(8); + } + else + { + m_type = MOD_SINGLEFILE; + } + m_name = name_base; + } +} + +bool Mod::enable(bool value) +{ + if (m_type == Mod::MOD_UNKNOWN || m_type == Mod::MOD_FOLDER) + return false; + + if (m_enabled == value) + return false; + + QString path = m_file.absoluteFilePath(); + if (value) + { + QFile foo(path); + if (!path.endsWith(".disabled")) + return false; + path.chop(9); + if (!foo.rename(path)) + return false; + } + else + { + QFile foo(path); + path += ".disabled"; + if (!foo.rename(path)) + return false; + } + repath(QFileInfo(path)); + m_enabled = value; + return true; +} + +bool Mod::destroy() +{ + m_type = MOD_UNKNOWN; + return FS::deletePath(m_file.filePath()); +} + + +const ModDetails & Mod::details() const +{ + if(!m_localDetails) + return invalidDetails; + return *m_localDetails; +} + + +QString Mod::version() const +{ + return details().version; +} + +QString Mod::name() const +{ + auto & d = details(); + if(!d.name.isEmpty()) { + return d.name; + } + return m_name; +} + +QString Mod::homeurl() const +{ + return details().homeurl; +} + +QString Mod::description() const +{ + return details().description; +} + +QStringList Mod::authors() const +{ + return details().authors; +} diff --git a/ultimmc/launcher/minecraft/mod/Mod.h b/ultimmc/launcher/minecraft/mod/Mod.h new file mode 100644 index 0000000..921faeb --- /dev/null +++ b/ultimmc/launcher/minecraft/mod/Mod.h @@ -0,0 +1,115 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +#include "ModDetails.h" + + + +class Mod +{ +public: + enum ModType + { + MOD_UNKNOWN, //!< Indicates an unspecified mod type. + MOD_ZIPFILE, //!< The mod is a zip file containing the mod's class files. + MOD_SINGLEFILE, //!< The mod is a single file (not a zip file). + MOD_FOLDER, //!< The mod is in a folder on the filesystem. + MOD_LITEMOD, //!< The mod is a litemod + }; + + Mod() = default; + Mod(const QFileInfo &file); + + QFileInfo filename() const + { + return m_file; + } + QString mmc_id() const + { + return m_mmc_id; + } + ModType type() const + { + return m_type; + } + bool valid() + { + return m_type != MOD_UNKNOWN; + } + + QDateTime dateTimeChanged() const + { + return m_changedDateTime; + } + + bool enabled() const + { + return m_enabled; + } + + const ModDetails &details() const; + + QString name() const; + QString version() const; + QString homeurl() const; + QString description() const; + QStringList authors() const; + + bool enable(bool value); + + // delete all the files of this mod + bool destroy(); + + // change the mod's filesystem path (used by mod lists for *MAGIC* purposes) + void repath(const QFileInfo &file); + + bool shouldResolve() { + return !m_resolving && !m_resolved; + } + bool isResolving() { + return m_resolving; + } + int resolutionTicket() + { + return m_resolutionTicket; + } + void setResolving(bool resolving, int resolutionTicket) { + m_resolving = resolving; + m_resolutionTicket = resolutionTicket; + } + void finishResolvingWithDetails(std::shared_ptr details){ + m_resolving = false; + m_resolved = true; + m_localDetails = details; + } + +protected: + QFileInfo m_file; + QDateTime m_changedDateTime; + QString m_mmc_id; + QString m_name; + bool m_enabled = true; + bool m_resolving = false; + bool m_resolved = false; + int m_resolutionTicket = 0; + ModType m_type = MOD_UNKNOWN; + std::shared_ptr m_localDetails; +}; diff --git a/ultimmc/launcher/minecraft/mod/ModDetails.h b/ultimmc/launcher/minecraft/mod/ModDetails.h new file mode 100644 index 0000000..6ab4aee --- /dev/null +++ b/ultimmc/launcher/minecraft/mod/ModDetails.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +struct ModDetails +{ + QString mod_id; + QString name; + QString version; + QString mcversion; + QString homeurl; + QString updateurl; + QString description; + QStringList authors; + QString credits; +}; diff --git a/ultimmc/launcher/minecraft/mod/ModFolderLoadTask.cpp b/ultimmc/launcher/minecraft/mod/ModFolderLoadTask.cpp new file mode 100644 index 0000000..8834987 --- /dev/null +++ b/ultimmc/launcher/minecraft/mod/ModFolderLoadTask.cpp @@ -0,0 +1,18 @@ +#include "ModFolderLoadTask.h" +#include + +ModFolderLoadTask::ModFolderLoadTask(QDir dir) : + m_dir(dir), m_result(new Result()) +{ +} + +void ModFolderLoadTask::run() +{ + m_dir.refresh(); + for (auto entry : m_dir.entryInfoList()) + { + Mod m(entry); + m_result->mods[m.mmc_id()] = m; + } + emit succeeded(); +} diff --git a/ultimmc/launcher/minecraft/mod/ModFolderLoadTask.h b/ultimmc/launcher/minecraft/mod/ModFolderLoadTask.h new file mode 100644 index 0000000..8d720e6 --- /dev/null +++ b/ultimmc/launcher/minecraft/mod/ModFolderLoadTask.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include +#include +#include +#include "Mod.h" +#include + +class ModFolderLoadTask : public QObject, public QRunnable +{ + Q_OBJECT +public: + struct Result { + QMap mods; + }; + using ResultPtr = std::shared_ptr; + ResultPtr result() const { + return m_result; + } + +public: + ModFolderLoadTask(QDir dir); + void run(); +signals: + void succeeded(); +private: + QDir m_dir; + ResultPtr m_result; +}; diff --git a/ultimmc/launcher/minecraft/mod/ModFolderModel.cpp b/ultimmc/launcher/minecraft/mod/ModFolderModel.cpp new file mode 100644 index 0000000..f0c53c3 --- /dev/null +++ b/ultimmc/launcher/minecraft/mod/ModFolderModel.cpp @@ -0,0 +1,554 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModFolderModel.h" +#include +#include +#include +#include +#include +#include +#include +#include "ModFolderLoadTask.h" +#include +#include +#include "LocalModParseTask.h" + +ModFolderModel::ModFolderModel(const QString &dir) : QAbstractListModel(), m_dir(dir) +{ + FS::ensureFolderPathExists(m_dir.absolutePath()); + m_dir.setFilter(QDir::Readable | QDir::NoDotAndDotDot | QDir::Files | QDir::Dirs); + m_dir.setSorting(QDir::Name | QDir::IgnoreCase | QDir::LocaleAware); + m_watcher = new QFileSystemWatcher(this); + connect(m_watcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); +} + +void ModFolderModel::startWatching() +{ + if(is_watching) + return; + + update(); + + is_watching = m_watcher->addPath(m_dir.absolutePath()); + if (is_watching) + { + qDebug() << "Started watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to start watching " << m_dir.absolutePath(); + } +} + +void ModFolderModel::stopWatching() +{ + if(!is_watching) + return; + + is_watching = !m_watcher->removePath(m_dir.absolutePath()); + if (!is_watching) + { + qDebug() << "Stopped watching " << m_dir.absolutePath(); + } + else + { + qDebug() << "Failed to stop watching " << m_dir.absolutePath(); + } +} + +bool ModFolderModel::update() +{ + if (!isValid()) { + return false; + } + if(m_update) { + scheduled_update = true; + return true; + } + + auto task = new ModFolderLoadTask(m_dir); + m_update = task->result(); + QThreadPool *threadPool = QThreadPool::globalInstance(); + connect(task, &ModFolderLoadTask::succeeded, this, &ModFolderModel::finishUpdate); + threadPool->start(task); + return true; +} + +void ModFolderModel::finishUpdate() +{ + QSet currentSet = modsIndex.keys().toSet(); + auto & newMods = m_update->mods; + QSet newSet = newMods.keys().toSet(); + + // see if the kept mods changed in some way + { + QSet kept = currentSet; + kept.intersect(newSet); + for(auto & keptMod: kept) { + auto & newMod = newMods[keptMod]; + auto row = modsIndex[keptMod]; + auto & currentMod = mods[row]; + if(newMod.dateTimeChanged() == currentMod.dateTimeChanged()) { + // no significant change, ignore... + continue; + } + auto & oldMod = mods[row]; + if(oldMod.isResolving()) { + activeTickets.remove(oldMod.resolutionTicket()); + } + oldMod = newMod; + resolveMod(mods[row]); + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + } + } + + // remove mods no longer present + { + QSet removed = currentSet; + QList removedRows; + removed.subtract(newSet); + for(auto & removedMod: removed) { + removedRows.append(modsIndex[removedMod]); + } + std::sort(removedRows.begin(), removedRows.end(), std::greater()); + for(auto iter = removedRows.begin(); iter != removedRows.end(); iter++) { + int removedIndex = *iter; + beginRemoveRows(QModelIndex(), removedIndex, removedIndex); + auto removedIter = mods.begin() + removedIndex; + if(removedIter->isResolving()) { + activeTickets.remove(removedIter->resolutionTicket()); + } + mods.erase(removedIter); + endRemoveRows(); + } + } + + // add new mods to the end + { + QSet added = newSet; + added.subtract(currentSet); + beginInsertRows(QModelIndex(), mods.size(), mods.size() + added.size() - 1); + for(auto & addedMod: added) { + mods.append(newMods[addedMod]); + resolveMod(mods.last()); + } + endInsertRows(); + } + + // update index + { + modsIndex.clear(); + int idx = 0; + for(auto & mod: mods) { + modsIndex[mod.mmc_id()] = idx; + idx++; + } + } + + m_update.reset(); + + emit updateFinished(); + + if(scheduled_update) { + scheduled_update = false; + update(); + } +} + +void ModFolderModel::resolveMod(Mod& m) +{ + if(!m.shouldResolve()) { + return; + } + + auto task = new LocalModParseTask(nextResolutionTicket, m.type(), m.filename()); + auto result = task->result(); + result->id = m.mmc_id(); + activeTickets.insert(nextResolutionTicket, result); + m.setResolving(true, nextResolutionTicket); + nextResolutionTicket++; + QThreadPool *threadPool = QThreadPool::globalInstance(); + connect(task, &LocalModParseTask::finished, this, &ModFolderModel::finishModParse); + threadPool->start(task); +} + +void ModFolderModel::finishModParse(int token) +{ + auto iter = activeTickets.find(token); + if(iter == activeTickets.end()) { + return; + } + auto result = *iter; + activeTickets.remove(token); + int row = modsIndex[result->id]; + auto & mod = mods[row]; + mod.finishResolvingWithDetails(result->details); + emit dataChanged(index(row), index(row, columnCount(QModelIndex()) - 1)); +} + +void ModFolderModel::disableInteraction(bool disabled) +{ + if (interaction_disabled == disabled) { + return; + } + interaction_disabled = disabled; + if(size()) { + emit dataChanged(index(0), index(size() - 1)); + } +} + +void ModFolderModel::directoryChanged(QString path) +{ + update(); +} + +bool ModFolderModel::isValid() +{ + return m_dir.exists() && m_dir.isReadable(); +} + +// FIXME: this does not take disabled mod (with extra .disable extension) into account... +bool ModFolderModel::installMod(const QString &filename) +{ + if(interaction_disabled) { + return false; + } + + // NOTE: fix for GH-1178: remove trailing slash to avoid issues with using the empty result of QFileInfo::fileName + auto originalPath = FS::NormalizePath(filename); + QFileInfo fileinfo(originalPath); + + if (!fileinfo.exists() || !fileinfo.isReadable()) + { + qWarning() << "Caught attempt to install non-existing file or file-like object:" << originalPath; + return false; + } + qDebug() << "installing: " << fileinfo.absoluteFilePath(); + + Mod installedMod(fileinfo); + if (!installedMod.valid()) + { + qDebug() << originalPath << "is not a valid mod. Ignoring it."; + return false; + } + + auto type = installedMod.type(); + if (type == Mod::MOD_UNKNOWN) + { + qDebug() << "Cannot recognize mod type of" << originalPath << ", ignoring it."; + return false; + } + + auto newpath = FS::NormalizePath(FS::PathCombine(m_dir.path(), fileinfo.fileName())); + if(originalPath == newpath) + { + qDebug() << "Overwriting the mod (" << originalPath << ") with itself makes no sense..."; + return false; + } + + if (type == Mod::MOD_SINGLEFILE || type == Mod::MOD_ZIPFILE || type == Mod::MOD_LITEMOD) + { + if(QFile::exists(newpath) || QFile::exists(newpath + QString(".disabled"))) + { + if(!QFile::remove(newpath)) + { + // FIXME: report error in a user-visible way + qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed."; + return false; + } + qDebug() << newpath << "has been deleted."; + } + if (!QFile::copy(fileinfo.filePath(), newpath)) + { + qWarning() << "Copy from" << originalPath << "to" << newpath << "has failed."; + // FIXME: report error in a user-visible way + return false; + } + FS::updateTimestamp(newpath); + installedMod.repath(newpath); + update(); + return true; + } + else if (type == Mod::MOD_FOLDER) + { + QString from = fileinfo.filePath(); + if(QFile::exists(newpath)) + { + qDebug() << "Ignoring folder " << from << ", it would merge with " << newpath; + return false; + } + + if (!FS::copy(from, newpath)()) + { + qWarning() << "Copy of folder from" << originalPath << "to" << newpath << "has (potentially partially) failed."; + return false; + } + installedMod.repath(newpath); + update(); + return true; + } + return false; +} + +bool ModFolderModel::setModStatus(const QModelIndexList& indexes, ModStatusAction enable) +{ + if(interaction_disabled) { + return false; + } + + if(indexes.isEmpty()) + return true; + + for (auto index: indexes) + { + if(index.column() != 0) { + continue; + } + setModStatus(index.row(), enable); + } + return true; +} + +bool ModFolderModel::deleteMods(const QModelIndexList& indexes) +{ + if(interaction_disabled) { + return false; + } + + if(indexes.isEmpty()) + return true; + + for (auto i: indexes) + { + Mod &m = mods[i.row()]; + m.destroy(); + } + return true; +} + +int ModFolderModel::columnCount(const QModelIndex &parent) const +{ + return NUM_COLUMNS; +} + +QVariant ModFolderModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + + if (row < 0 || row >= mods.size()) + return QVariant(); + + switch (role) + { + case Qt::DisplayRole: + switch (column) + { + case NameColumn: + return mods[row].name(); + case VersionColumn: { + switch(mods[row].type()) { + case Mod::MOD_FOLDER: + return tr("Folder"); + case Mod::MOD_SINGLEFILE: + return tr("File"); + default: + break; + } + return mods[row].version(); + } + case DateColumn: + return mods[row].dateTimeChanged(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return mods[row].mmc_id(); + + case Qt::CheckStateRole: + switch (column) + { + case ActiveColumn: + return mods[row].enabled() ? Qt::Checked : Qt::Unchecked; + default: + return QVariant(); + } + default: + return QVariant(); + } +} + +bool ModFolderModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid()) + { + return false; + } + + if (role == Qt::CheckStateRole) + { + return setModStatus(index.row(), Toggle); + } + return false; +} + +bool ModFolderModel::setModStatus(int row, ModFolderModel::ModStatusAction action) +{ + if(row < 0 || row >= mods.size()) { + return false; + } + + auto &mod = mods[row]; + bool desiredStatus; + switch(action) { + case Enable: + desiredStatus = true; + break; + case Disable: + desiredStatus = false; + break; + case Toggle: + default: + desiredStatus = !mod.enabled(); + break; + } + + if(desiredStatus == mod.enabled()) { + return true; + } + + // preserve the row, but change its ID + auto oldId = mod.mmc_id(); + if(!mod.enable(!mod.enabled())) { + return false; + } + auto newId = mod.mmc_id(); + if(modsIndex.contains(newId)) { + // NOTE: this could handle a corner case, where we are overwriting a file, because the same 'mod' exists both enabled and disabled + // But is it necessary? + } + modsIndex.remove(oldId); + modsIndex[newId] = row; + emit dataChanged(index(row, 0), index(row, columnCount(QModelIndex()) - 1)); + return true; +} + +QVariant ModFolderModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + return QString(); + case NameColumn: + return tr("Name"); + case VersionColumn: + return tr("Version"); + case DateColumn: + return tr("Last changed"); + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case ActiveColumn: + return tr("Is the mod enabled?"); + case NameColumn: + return tr("The name of the mod."); + case VersionColumn: + return tr("The version of the mod."); + case DateColumn: + return tr("The date and time this mod was last changed (or added)."); + default: + return QVariant(); + } + default: + return QVariant(); + } + return QVariant(); +} + +Qt::ItemFlags ModFolderModel::flags(const QModelIndex &index) const +{ + Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index); + auto flags = defaultFlags; + if(interaction_disabled) { + flags &= ~Qt::ItemIsDropEnabled; + } + else + { + flags |= Qt::ItemIsDropEnabled; + if(index.isValid()) { + flags |= Qt::ItemIsUserCheckable; + } + } + return flags; +} + +Qt::DropActions ModFolderModel::supportedDropActions() const +{ + // copy from outside, move from within and other mod lists + return Qt::CopyAction | Qt::MoveAction; +} + +QStringList ModFolderModel::mimeTypes() const +{ + QStringList types; + types << "text/uri-list"; + return types; +} + +bool ModFolderModel::dropMimeData(const QMimeData* data, Qt::DropAction action, int, int, const QModelIndex&) +{ + if (action == Qt::IgnoreAction) + { + return true; + } + + // check if the action is supported + if (!data || !(action & supportedDropActions())) + { + return false; + } + + // files dropped from outside? + if (data->hasUrls()) + { + auto urls = data->urls(); + for (auto url : urls) + { + // only local files may be dropped... + if (!url.isLocalFile()) + { + continue; + } + // TODO: implement not only copy, but also move + // FIXME: handle errors here + installMod(url.toLocalFile()); + } + return true; + } + return false; +} diff --git a/ultimmc/launcher/minecraft/mod/ModFolderModel.h b/ultimmc/launcher/minecraft/mod/ModFolderModel.h new file mode 100644 index 0000000..62c504d --- /dev/null +++ b/ultimmc/launcher/minecraft/mod/ModFolderModel.h @@ -0,0 +1,148 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "Mod.h" + +#include "ModFolderLoadTask.h" +#include "LocalModParseTask.h" + +class LegacyInstance; +class BaseInstance; +class QFileSystemWatcher; + +/** + * A legacy mod list. + * Backed by a folder. + */ +class ModFolderModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum Columns + { + ActiveColumn = 0, + NameColumn, + VersionColumn, + DateColumn, + NUM_COLUMNS + }; + enum ModStatusAction { + Disable, + Enable, + Toggle + }; + ModFolderModel(const QString &dir); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + Qt::DropActions supportedDropActions() const override; + + /// flags, mostly to support drag&drop + virtual Qt::ItemFlags flags(const QModelIndex &index) const override; + QStringList mimeTypes() const override; + bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override; + + virtual int rowCount(const QModelIndex &) const override + { + return size(); + } + + virtual QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + virtual int columnCount(const QModelIndex &parent) const override; + + size_t size() const + { + return mods.size(); + } + ; + bool empty() const + { + return size() == 0; + } + Mod &operator[](size_t index) + { + return mods[index]; + } + const Mod &at(size_t index) const + { + return mods.at(index); + } + + /// Reloads the mod list and returns true if the list changed. + bool update(); + + /** + * Adds the given mod to the list at the given index - if the list supports custom ordering + */ + bool installMod(const QString& filename); + + /// Deletes all the selected mods + bool deleteMods(const QModelIndexList &indexes); + + /// Enable or disable listed mods + bool setModStatus(const QModelIndexList &indexes, ModStatusAction action); + + void startWatching(); + void stopWatching(); + + bool isValid(); + + QDir dir() + { + return m_dir; + } + + const QList & allMods() + { + return mods; + } + +public slots: + void disableInteraction(bool disabled); + +private +slots: + void directoryChanged(QString path); + void finishUpdate(); + void finishModParse(int token); + +signals: + void updateFinished(); + +private: + void resolveMod(Mod& m); + bool setModStatus(int index, ModStatusAction action); + +protected: + QFileSystemWatcher *m_watcher; + bool is_watching = false; + ModFolderLoadTask::ResultPtr m_update; + bool scheduled_update = false; + bool interaction_disabled = false; + QDir m_dir; + QMap modsIndex; + QMap activeTickets; + int nextResolutionTicket = 0; + QList mods; +}; diff --git a/ultimmc/launcher/minecraft/mod/ModFolderModel_test.cpp b/ultimmc/launcher/minecraft/mod/ModFolderModel_test.cpp new file mode 100644 index 0000000..76f16ed --- /dev/null +++ b/ultimmc/launcher/minecraft/mod/ModFolderModel_test.cpp @@ -0,0 +1,53 @@ + +#include +#include +#include "TestUtil.h" + +#include "FileSystem.h" +#include "minecraft/mod/ModFolderModel.h" + +class ModFolderModelTest : public QObject +{ + Q_OBJECT + +private +slots: + // test for GH-1178 - install a folder with files to a mod list + void test_1178() + { + // source + QString source = QFINDTESTDATA("data/test_folder"); + + // sanity check + QVERIFY(!source.endsWith('/')); + + auto verify = [](QString path) + { + QDir target_dir(FS::PathCombine(path, "test_folder")); + QVERIFY(target_dir.entryList().contains("pack.mcmeta")); + QVERIFY(target_dir.entryList().contains("assets")); + }; + + // 1. test with no trailing / + { + QString folder = source; + QTemporaryDir tempDir; + ModFolderModel m(tempDir.path()); + m.installMod(folder); + verify(tempDir.path()); + } + + // 2. test with trailing / + { + QString folder = source + '/'; + QTemporaryDir tempDir; + ModFolderModel m(tempDir.path()); + m.installMod(folder); + verify(tempDir.path()); + } + } +}; + +QTEST_GUILESS_MAIN(ModFolderModelTest) + +#include "ModFolderModel_test.moc" diff --git a/ultimmc/launcher/minecraft/mod/ResourcePackFolderModel.cpp b/ultimmc/launcher/minecraft/mod/ResourcePackFolderModel.cpp new file mode 100644 index 0000000..f3d7f56 --- /dev/null +++ b/ultimmc/launcher/minecraft/mod/ResourcePackFolderModel.cpp @@ -0,0 +1,23 @@ +#include "ResourcePackFolderModel.h" + +ResourcePackFolderModel::ResourcePackFolderModel(const QString &dir) : ModFolderModel(dir) { +} + +QVariant ResourcePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::ToolTipRole) { + switch (section) { + case ActiveColumn: + return tr("Is the resource pack enabled?"); + case NameColumn: + return tr("The name of the resource pack."); + case VersionColumn: + return tr("The version of the resource pack."); + case DateColumn: + return tr("The date and time this resource pack was last changed (or added)."); + default: + return QVariant(); + } + } + + return ModFolderModel::headerData(section, orientation, role); +} diff --git a/ultimmc/launcher/minecraft/mod/ResourcePackFolderModel.h b/ultimmc/launcher/minecraft/mod/ResourcePackFolderModel.h new file mode 100644 index 0000000..0cd6214 --- /dev/null +++ b/ultimmc/launcher/minecraft/mod/ResourcePackFolderModel.h @@ -0,0 +1,13 @@ +#pragma once + +#include "ModFolderModel.h" + +class ResourcePackFolderModel : public ModFolderModel +{ + Q_OBJECT + +public: + explicit ResourcePackFolderModel(const QString &dir); + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; +}; diff --git a/ultimmc/launcher/minecraft/mod/TexturePackFolderModel.cpp b/ultimmc/launcher/minecraft/mod/TexturePackFolderModel.cpp new file mode 100644 index 0000000..d5956da --- /dev/null +++ b/ultimmc/launcher/minecraft/mod/TexturePackFolderModel.cpp @@ -0,0 +1,23 @@ +#include "TexturePackFolderModel.h" + +TexturePackFolderModel::TexturePackFolderModel(const QString &dir) : ModFolderModel(dir) { +} + +QVariant TexturePackFolderModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::ToolTipRole) { + switch (section) { + case ActiveColumn: + return tr("Is the texture pack enabled?"); + case NameColumn: + return tr("The name of the texture pack."); + case VersionColumn: + return tr("The version of the texture pack."); + case DateColumn: + return tr("The date and time this texture pack was last changed (or added)."); + default: + return QVariant(); + } + } + + return ModFolderModel::headerData(section, orientation, role); +} diff --git a/ultimmc/launcher/minecraft/mod/TexturePackFolderModel.h b/ultimmc/launcher/minecraft/mod/TexturePackFolderModel.h new file mode 100644 index 0000000..a59d511 --- /dev/null +++ b/ultimmc/launcher/minecraft/mod/TexturePackFolderModel.h @@ -0,0 +1,13 @@ +#pragma once + +#include "ModFolderModel.h" + +class TexturePackFolderModel : public ModFolderModel +{ + Q_OBJECT + +public: + explicit TexturePackFolderModel(const QString &dir); + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; +}; diff --git a/ultimmc/launcher/minecraft/services/CapeChange.cpp b/ultimmc/launcher/minecraft/services/CapeChange.cpp new file mode 100644 index 0000000..e49c166 --- /dev/null +++ b/ultimmc/launcher/minecraft/services/CapeChange.cpp @@ -0,0 +1,69 @@ +#include "CapeChange.h" + +#include +#include + +#include "Application.h" + +CapeChange::CapeChange(QObject *parent, QString token, QString cape) + : Task(parent), m_capeId(cape), m_token(token) +{ +} + +void CapeChange::setCape(QString& cape) { + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); + auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); + QNetworkReply *rep = APPLICATION->network()->put(request, requestString.toUtf8()); + + setStatus(tr("Equipping cape")); + + m_reply = shared_qobject_ptr(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + +void CapeChange::clearCape() { + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/capes/active")); + auto requestString = QString("{\"capeId\":\"%1\"}").arg(m_capeId); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); + QNetworkReply *rep = APPLICATION->network()->deleteResource(request); + + setStatus(tr("Removing cape")); + + m_reply = shared_qobject_ptr(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + + +void CapeChange::executeTask() +{ + if(m_capeId.isEmpty()) { + clearCape(); + } + else { + setCape(m_capeId); + } +} + +void CapeChange::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + qCritical() << "Network error: " << error; + emitFailed(m_reply->errorString()); +} + +void CapeChange::downloadFinished() +{ + // if the download failed + if (m_reply->error() != QNetworkReply::NetworkError::NoError) + { + emitFailed(QString("Network error: %1").arg(m_reply->errorString())); + m_reply.reset(); + return; + } + emitSucceeded(); +} diff --git a/ultimmc/launcher/minecraft/services/CapeChange.h b/ultimmc/launcher/minecraft/services/CapeChange.h new file mode 100644 index 0000000..185d69b --- /dev/null +++ b/ultimmc/launcher/minecraft/services/CapeChange.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include +#include "tasks/Task.h" +#include "QObjectPtr.h" + +class CapeChange : public Task +{ + Q_OBJECT +public: + CapeChange(QObject *parent, QString token, QString capeId); + virtual ~CapeChange() {} + +private: + void setCape(QString & cape); + void clearCape(); + +private: + QString m_capeId; + QString m_token; + shared_qobject_ptr m_reply; + +protected: + virtual void executeTask(); + +public slots: + void downloadError(QNetworkReply::NetworkError); + void downloadFinished(); +}; + diff --git a/ultimmc/launcher/minecraft/services/SkinDelete.cpp b/ultimmc/launcher/minecraft/services/SkinDelete.cpp new file mode 100644 index 0000000..cce8364 --- /dev/null +++ b/ultimmc/launcher/minecraft/services/SkinDelete.cpp @@ -0,0 +1,44 @@ +#include "SkinDelete.h" + +#include +#include + +#include "Application.h" + +SkinDelete::SkinDelete(QObject *parent, QString token) + : Task(parent), m_token(token) +{ +} + +void SkinDelete::executeTask() +{ + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins/active")); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); + QNetworkReply *rep = APPLICATION->network()->deleteResource(request); + m_reply = shared_qobject_ptr(rep); + + setStatus(tr("Deleting skin")); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + +void SkinDelete::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + qCritical() << "Network error: " << error; + emitFailed(m_reply->errorString()); +} + +void SkinDelete::downloadFinished() +{ + // if the download failed + if (m_reply->error() != QNetworkReply::NetworkError::NoError) + { + emitFailed(QString("Network error: %1").arg(m_reply->errorString())); + m_reply.reset(); + return; + } + emitSucceeded(); +} + diff --git a/ultimmc/launcher/minecraft/services/SkinDelete.h b/ultimmc/launcher/minecraft/services/SkinDelete.h new file mode 100644 index 0000000..83a8468 --- /dev/null +++ b/ultimmc/launcher/minecraft/services/SkinDelete.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include "tasks/Task.h" + +typedef shared_qobject_ptr SkinDeletePtr; + +class SkinDelete : public Task +{ + Q_OBJECT +public: + SkinDelete(QObject *parent, QString token); + virtual ~SkinDelete() = default; + +private: + QString m_token; + shared_qobject_ptr m_reply; + +protected: + virtual void executeTask(); + +public slots: + void downloadError(QNetworkReply::NetworkError); + void downloadFinished(); +}; diff --git a/ultimmc/launcher/minecraft/services/SkinUpload.cpp b/ultimmc/launcher/minecraft/services/SkinUpload.cpp new file mode 100644 index 0000000..7c2e833 --- /dev/null +++ b/ultimmc/launcher/minecraft/services/SkinUpload.cpp @@ -0,0 +1,68 @@ +#include "SkinUpload.h" + +#include +#include + +#include "Application.h" + +QByteArray getVariant(SkinUpload::Model model) { + switch (model) { + default: + qDebug() << "Unknown skin type!"; + case SkinUpload::STEVE: + return "CLASSIC"; + case SkinUpload::ALEX: + return "SLIM"; + } +} + +SkinUpload::SkinUpload(QObject *parent, QString token, QByteArray skin, SkinUpload::Model model) + : Task(parent), m_model(model), m_skin(skin), m_token(token) +{ +} + +void SkinUpload::executeTask() +{ + QNetworkRequest request(QUrl("https://api.minecraftservices.com/minecraft/profile/skins")); + request.setRawHeader("Authorization", QString("Bearer %1").arg(m_token).toLocal8Bit()); + QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + + QHttpPart skin; + skin.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("image/png")); + skin.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"skin.png\"")); + skin.setBody(m_skin); + + QHttpPart model; + model.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"variant\"")); + model.setBody(getVariant(m_model)); + + multiPart->append(skin); + multiPart->append(model); + + QNetworkReply *rep = APPLICATION->network()->post(request, multiPart); + m_reply = shared_qobject_ptr(rep); + + setStatus(tr("Uploading skin")); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + +void SkinUpload::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + qCritical() << "Network error: " << error; + emitFailed(m_reply->errorString()); +} + +void SkinUpload::downloadFinished() +{ + // if the download failed + if (m_reply->error() != QNetworkReply::NetworkError::NoError) + { + emitFailed(QString("Network error: %1").arg(m_reply->errorString())); + m_reply.reset(); + return; + } + emitSucceeded(); +} diff --git a/ultimmc/launcher/minecraft/services/SkinUpload.h b/ultimmc/launcher/minecraft/services/SkinUpload.h new file mode 100644 index 0000000..2c1f0a2 --- /dev/null +++ b/ultimmc/launcher/minecraft/services/SkinUpload.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include "tasks/Task.h" + +typedef shared_qobject_ptr SkinUploadPtr; + +class SkinUpload : public Task +{ + Q_OBJECT +public: + enum Model + { + STEVE, + ALEX + }; + + // Note this class takes ownership of the file. + SkinUpload(QObject *parent, QString token, QByteArray skin, Model model = STEVE); + virtual ~SkinUpload() {} + +private: + Model m_model; + QByteArray m_skin; + QString m_token; + shared_qobject_ptr m_reply; +protected: + virtual void executeTask(); + +public slots: + + void downloadError(QNetworkReply::NetworkError); + + void downloadFinished(); +}; diff --git a/ultimmc/launcher/minecraft/testdata/1.9-simple.json b/ultimmc/launcher/minecraft/testdata/1.9-simple.json new file mode 100644 index 0000000..574c5b0 --- /dev/null +++ b/ultimmc/launcher/minecraft/testdata/1.9-simple.json @@ -0,0 +1,198 @@ +{ + "assets": "1.9", + "id": "1.9", + "libraries": [ + { + "name": "oshi-project:oshi-core:1.1" + }, + { + "name": "net.java.dev.jna:jna:3.4.0" + }, + { + "name": "net.java.dev.jna:platform:3.4.0" + }, + { + "name": "com.ibm.icu:icu4j-core-mojang:51.2" + }, + { + "name": "net.sf.jopt-simple:jopt-simple:4.6" + }, + { + "name": "com.paulscode:codecjorbis:20101023" + }, + { + "name": "com.paulscode:codecwav:20101023" + }, + { + "name": "com.paulscode:libraryjavasound:20101123" + }, + { + "name": "com.paulscode:librarylwjglopenal:20100824" + }, + { + "name": "com.paulscode:soundsystem:20120107" + }, + { + "name": "io.netty:netty-all:4.0.23.Final" + }, + { + "name": "com.google.guava:guava:17.0" + }, + { + "name": "org.apache.commons:commons-lang3:3.3.2" + }, + { + "name": "commons-io:commons-io:2.4" + }, + { + "name": "commons-codec:commons-codec:1.9" + }, + { + "name": "net.java.jinput:jinput:2.0.5" + }, + { + "name": "net.java.jutils:jutils:1.0.0" + }, + { + "name": "com.google.code.gson:gson:2.2.4" + }, + { + "name": "com.mojang:authlib:1.5.22" + }, + { + "name": "com.mojang:realms:1.8.4" + }, + { + "name": "org.apache.commons:commons-compress:1.8.1" + }, + { + "name": "org.apache.httpcomponents:httpclient:4.3.3" + }, + { + "name": "commons-logging:commons-logging:1.1.3" + }, + { + "name": "org.apache.httpcomponents:httpcore:4.3.2" + }, + { + "name": "org.apache.logging.log4j:log4j-api:2.0-beta9" + }, + { + "name": "org.apache.logging.log4j:log4j-core:2.0-beta9" + }, + { + "name": "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209", + "rules": [ + { + "action": "allow" + }, + { + "action": "disallow", + "os": { + "name": "osx" + } + } + ] + }, + { + "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209", + "rules": [ + { + "action": "allow" + }, + { + "action": "disallow", + "os": { + "name": "osx" + } + } + ] + }, + { + "extract": { + "exclude": [ + "META-INF/" + ] + }, + "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209", + "natives": { + "linux": "natives-linux", + "osx": "natives-osx", + "windows": "natives-windows" + }, + "rules": [ + { + "action": "allow" + }, + { + "action": "disallow", + "os": { + "name": "osx" + } + } + ] + }, + { + "name": "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822", + "rules": [ + { + "action": "allow", + "os": { + "name": "osx" + } + } + ] + }, + { + "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822", + "rules": [ + { + "action": "allow", + "os": { + "name": "osx" + } + } + ] + }, + { + "extract": { + "exclude": [ + "META-INF/" + ] + }, + "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822", + "natives": { + "linux": "natives-linux", + "osx": "natives-osx", + "windows": "natives-windows" + }, + "rules": [ + { + "action": "allow", + "os": { + "name": "osx" + } + } + ] + }, + { + "extract": { + "exclude": [ + "META-INF/" + ] + }, + "name": "net.java.jinput:jinput-platform:2.0.5", + "natives": { + "linux": "natives-linux", + "osx": "natives-osx", + "windows": "natives-windows" + } + } + ], + "mainClass": "net.minecraft.client.main.Main", + "minecraftArguments": "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}", + "minimumLauncherVersion": 18, + "releaseTime": "2016-02-29T13:49:54+00:00", + "time": "2016-03-01T13:14:53+00:00", + "type": "release" +} diff --git a/ultimmc/launcher/minecraft/testdata/1.9.json b/ultimmc/launcher/minecraft/testdata/1.9.json new file mode 100644 index 0000000..697c605 --- /dev/null +++ b/ultimmc/launcher/minecraft/testdata/1.9.json @@ -0,0 +1,529 @@ +{ + "assetIndex": { + "id": "1.9", + "sha1": "cde65b47a43f638653ab1da3848b53f8a7477b16", + "size": 136916, + "totalSize": 119917473, + "url": "https://launchermeta.mojang.com/mc-staging/assets/1.9/cde65b47a43f638653ab1da3848b53f8a7477b16/1.9.json" + }, + "assets": "1.9", + "downloads": { + "client": { + "sha1": "2f67dfe8953299440d1902f9124f0f2c3a2c940f", + "size": 8697592, + "url": "https://launcher.mojang.com/mc/game/1.9/client/2f67dfe8953299440d1902f9124f0f2c3a2c940f/client.jar" + }, + "server": { + "sha1": "b4d449cf2918e0f3bd8aa18954b916a4d1880f0d", + "size": 8848015, + "url": "https://launcher.mojang.com/mc/game/1.9/server/b4d449cf2918e0f3bd8aa18954b916a4d1880f0d/server.jar" + } + }, + "id": "1.9", + "libraries": [ + { + "downloads": { + "artifact": { + "path": "oshi-project/oshi-core/1.1/oshi-core-1.1.jar", + "sha1": "9ddf7b048a8d701be231c0f4f95fd986198fd2d8", + "size": 30973, + "url": "https://libraries.minecraft.net/oshi-project/oshi-core/1.1/oshi-core-1.1.jar" + } + }, + "name": "oshi-project:oshi-core:1.1" + }, + { + "downloads": { + "artifact": { + "path": "net/java/dev/jna/jna/3.4.0/jna-3.4.0.jar", + "sha1": "803ff252fedbd395baffd43b37341dc4a150a554", + "size": 1008730, + "url": "https://libraries.minecraft.net/net/java/dev/jna/jna/3.4.0/jna-3.4.0.jar" + } + }, + "name": "net.java.dev.jna:jna:3.4.0" + }, + { + "downloads": { + "artifact": { + "path": "net/java/dev/jna/platform/3.4.0/platform-3.4.0.jar", + "sha1": "e3f70017be8100d3d6923f50b3d2ee17714e9c13", + "size": 913436, + "url": "https://libraries.minecraft.net/net/java/dev/jna/platform/3.4.0/platform-3.4.0.jar" + } + }, + "name": "net.java.dev.jna:platform:3.4.0" + }, + { + "downloads": { + "artifact": { + "path": "com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar", + "sha1": "63d216a9311cca6be337c1e458e587f99d382b84", + "size": 1634692, + "url": "https://libraries.minecraft.net/com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar" + } + }, + "name": "com.ibm.icu:icu4j-core-mojang:51.2" + }, + { + "downloads": { + "artifact": { + "path": "net/sf/jopt-simple/jopt-simple/4.6/jopt-simple-4.6.jar", + "sha1": "306816fb57cf94f108a43c95731b08934dcae15c", + "size": 62477, + "url": "https://libraries.minecraft.net/net/sf/jopt-simple/jopt-simple/4.6/jopt-simple-4.6.jar" + } + }, + "name": "net.sf.jopt-simple:jopt-simple:4.6" + }, + { + "downloads": { + "artifact": { + "path": "com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar", + "sha1": "c73b5636faf089d9f00e8732a829577de25237ee", + "size": 103871, + "url": "https://libraries.minecraft.net/com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar" + } + }, + "name": "com.paulscode:codecjorbis:20101023" + }, + { + "downloads": { + "artifact": { + "path": "com/paulscode/codecwav/20101023/codecwav-20101023.jar", + "sha1": "12f031cfe88fef5c1dd36c563c0a3a69bd7261da", + "size": 5618, + "url": "https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar" + } + }, + "name": "com.paulscode:codecwav:20101023" + }, + { + "downloads": { + "artifact": { + "path": "com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar", + "sha1": "5c5e304366f75f9eaa2e8cca546a1fb6109348b3", + "size": 21679, + "url": "https://libraries.minecraft.net/com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar" + } + }, + "name": "com.paulscode:libraryjavasound:20101123" + }, + { + "downloads": { + "artifact": { + "path": "com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar", + "sha1": "73e80d0794c39665aec3f62eee88ca91676674ef", + "size": 18981, + "url": "https://libraries.minecraft.net/com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar" + } + }, + "name": "com.paulscode:librarylwjglopenal:20100824" + }, + { + "downloads": { + "artifact": { + "path": "com/paulscode/soundsystem/20120107/soundsystem-20120107.jar", + "sha1": "419c05fe9be71f792b2d76cfc9b67f1ed0fec7f6", + "size": 65020, + "url": "https://libraries.minecraft.net/com/paulscode/soundsystem/20120107/soundsystem-20120107.jar" + } + }, + "name": "com.paulscode:soundsystem:20120107" + }, + { + "downloads": { + "artifact": { + "path": "io/netty/netty-all/4.0.23.Final/netty-all-4.0.23.Final.jar", + "sha1": "0294104aaf1781d6a56a07d561e792c5d0c95f45", + "size": 1779991, + "url": "https://libraries.minecraft.net/io/netty/netty-all/4.0.23.Final/netty-all-4.0.23.Final.jar" + } + }, + "name": "io.netty:netty-all:4.0.23.Final" + }, + { + "downloads": { + "artifact": { + "path": "com/google/guava/guava/17.0/guava-17.0.jar", + "sha1": "9c6ef172e8de35fd8d4d8783e4821e57cdef7445", + "size": 2243036, + "url": "https://libraries.minecraft.net/com/google/guava/guava/17.0/guava-17.0.jar" + } + }, + "name": "com.google.guava:guava:17.0" + }, + { + "downloads": { + "artifact": { + "path": "org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.jar", + "sha1": "90a3822c38ec8c996e84c16a3477ef632cbc87a3", + "size": 412739, + "url": "https://libraries.minecraft.net/org/apache/commons/commons-lang3/3.3.2/commons-lang3-3.3.2.jar" + } + }, + "name": "org.apache.commons:commons-lang3:3.3.2" + }, + { + "downloads": { + "artifact": { + "path": "commons-io/commons-io/2.4/commons-io-2.4.jar", + "sha1": "b1b6ea3b7e4aa4f492509a4952029cd8e48019ad", + "size": 185140, + "url": "https://libraries.minecraft.net/commons-io/commons-io/2.4/commons-io-2.4.jar" + } + }, + "name": "commons-io:commons-io:2.4" + }, + { + "downloads": { + "artifact": { + "path": "commons-codec/commons-codec/1.9/commons-codec-1.9.jar", + "sha1": "9ce04e34240f674bc72680f8b843b1457383161a", + "size": 263965, + "url": "https://libraries.minecraft.net/commons-codec/commons-codec/1.9/commons-codec-1.9.jar" + } + }, + "name": "commons-codec:commons-codec:1.9" + }, + { + "downloads": { + "artifact": { + "path": "net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar", + "sha1": "39c7796b469a600f72380316f6b1f11db6c2c7c4", + "size": 208338, + "url": "https://libraries.minecraft.net/net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar" + } + }, + "name": "net.java.jinput:jinput:2.0.5" + }, + { + "downloads": { + "artifact": { + "path": "net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar", + "sha1": "e12fe1fda814bd348c1579329c86943d2cd3c6a6", + "size": 7508, + "url": "https://libraries.minecraft.net/net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar" + } + }, + "name": "net.java.jutils:jutils:1.0.0" + }, + { + "downloads": { + "artifact": { + "path": "com/google/code/gson/gson/2.2.4/gson-2.2.4.jar", + "sha1": "a60a5e993c98c864010053cb901b7eab25306568", + "size": 190432, + "url": "https://libraries.minecraft.net/com/google/code/gson/gson/2.2.4/gson-2.2.4.jar" + } + }, + "name": "com.google.code.gson:gson:2.2.4" + }, + { + "downloads": { + "artifact": { + "path": "com/mojang/authlib/1.5.22/authlib-1.5.22.jar", + "sha1": "afaa8f6df976fcb5520e76ef1d5798c9e6b5c0b2", + "size": 64539, + "url": "https://libraries.minecraft.net/com/mojang/authlib/1.5.22/authlib-1.5.22.jar" + } + }, + "name": "com.mojang:authlib:1.5.22" + }, + { + "downloads": { + "artifact": { + "path": "com/mojang/realms/1.8.4/realms-1.8.4.jar", + "sha1": "15f8dc326c97a96dee6e65392e145ad6d1cb46cb", + "size": 1131574, + "url": "https://libraries.minecraft.net/com/mojang/realms/1.8.4/realms-1.8.4.jar" + } + }, + "name": "com.mojang:realms:1.8.4" + }, + { + "downloads": { + "artifact": { + "path": "org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar", + "sha1": "a698750c16740fd5b3871425f4cb3bbaa87f529d", + "size": 365552, + "url": "https://libraries.minecraft.net/org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar" + } + }, + "name": "org.apache.commons:commons-compress:1.8.1" + }, + { + "downloads": { + "artifact": { + "path": "org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar", + "sha1": "18f4247ff4572a074444572cee34647c43e7c9c7", + "size": 589512, + "url": "https://libraries.minecraft.net/org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar" + } + }, + "name": "org.apache.httpcomponents:httpclient:4.3.3" + }, + { + "downloads": { + "artifact": { + "path": "commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar", + "sha1": "f6f66e966c70a83ffbdb6f17a0919eaf7c8aca7f", + "size": 62050, + "url": "https://libraries.minecraft.net/commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar" + } + }, + "name": "commons-logging:commons-logging:1.1.3" + }, + { + "downloads": { + "artifact": { + "path": "org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar", + "sha1": "31fbbff1ddbf98f3aa7377c94d33b0447c646b6e", + "size": 282269, + "url": "https://libraries.minecraft.net/org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar" + } + }, + "name": "org.apache.httpcomponents:httpcore:4.3.2" + }, + { + "downloads": { + "artifact": { + "path": "org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar", + "sha1": "1dd66e68cccd907880229f9e2de1314bd13ff785", + "size": 108161, + "url": "https://libraries.minecraft.net/org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar" + } + }, + "name": "org.apache.logging.log4j:log4j-api:2.0-beta9" + }, + { + "downloads": { + "artifact": { + "path": "org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar", + "sha1": "678861ba1b2e1fccb594bb0ca03114bb05da9695", + "size": 681134, + "url": "https://libraries.minecraft.net/org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar" + } + }, + "name": "org.apache.logging.log4j:log4j-core:2.0-beta9" + }, + { + "downloads": { + "artifact": { + "path": "org/lwjgl/lwjgl/lwjgl/2.9.4-nightly-20150209/lwjgl-2.9.4-nightly-20150209.jar", + "sha1": "697517568c68e78ae0b4544145af031c81082dfe", + "size": 1047168, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl/2.9.4-nightly-20150209/lwjgl-2.9.4-nightly-20150209.jar" + } + }, + "name": "org.lwjgl.lwjgl:lwjgl:2.9.4-nightly-20150209", + "rules": [ + { + "action": "allow" + }, + { + "action": "disallow", + "os": { + "name": "osx" + } + } + ] + }, + { + "downloads": { + "artifact": { + "path": "org/lwjgl/lwjgl/lwjgl_util/2.9.4-nightly-20150209/lwjgl_util-2.9.4-nightly-20150209.jar", + "sha1": "d51a7c040a721d13efdfbd34f8b257b2df882ad0", + "size": 173887, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl_util/2.9.4-nightly-20150209/lwjgl_util-2.9.4-nightly-20150209.jar" + } + }, + "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.4-nightly-20150209", + "rules": [ + { + "action": "allow" + }, + { + "action": "disallow", + "os": { + "name": "osx" + } + } + ] + }, + { + "downloads": { + "artifact": { + "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar", + "sha1": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33", + "size": 22, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar" + }, + "classifiers": { + "natives-linux": { + "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar", + "sha1": "931074f46c795d2f7b30ed6395df5715cfd7675b", + "size": 578680, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar" + }, + "natives-osx": { + "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar", + "sha1": "bcab850f8f487c3f4c4dbabde778bb82bd1a40ed", + "size": 426822, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar" + }, + "natives-windows": { + "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar", + "sha1": "b84d5102b9dbfabfeb5e43c7e2828d98a7fc80e0", + "size": 613748, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar" + } + } + }, + "extract": { + "exclude": [ + "META-INF/" + ] + }, + "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209", + "natives": { + "linux": "natives-linux", + "osx": "natives-osx", + "windows": "natives-windows" + }, + "rules": [ + { + "action": "allow" + }, + { + "action": "disallow", + "os": { + "name": "osx" + } + } + ] + }, + { + "downloads": { + "artifact": { + "path": "org/lwjgl/lwjgl/lwjgl/2.9.2-nightly-20140822/lwjgl-2.9.2-nightly-20140822.jar", + "sha1": "7707204c9ffa5d91662de95f0a224e2f721b22af", + "size": 1045632, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl/2.9.2-nightly-20140822/lwjgl-2.9.2-nightly-20140822.jar" + } + }, + "name": "org.lwjgl.lwjgl:lwjgl:2.9.2-nightly-20140822", + "rules": [ + { + "action": "allow", + "os": { + "name": "osx" + } + } + ] + }, + { + "downloads": { + "artifact": { + "path": "org/lwjgl/lwjgl/lwjgl_util/2.9.2-nightly-20140822/lwjgl_util-2.9.2-nightly-20140822.jar", + "sha1": "f0e612c840a7639c1f77f68d72a28dae2f0c8490", + "size": 173887, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl_util/2.9.2-nightly-20140822/lwjgl_util-2.9.2-nightly-20140822.jar" + } + }, + "name": "org.lwjgl.lwjgl:lwjgl_util:2.9.2-nightly-20140822", + "rules": [ + { + "action": "allow", + "os": { + "name": "osx" + } + } + ] + }, + { + "downloads": { + "classifiers": { + "natives-linux": { + "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-linux.jar", + "sha1": "d898a33b5d0a6ef3fed3a4ead506566dce6720a5", + "size": 578539, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-linux.jar" + }, + "natives-osx": { + "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-osx.jar", + "sha1": "79f5ce2fea02e77fe47a3c745219167a542121d7", + "size": 468116, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-osx.jar" + }, + "natives-windows": { + "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-windows.jar", + "sha1": "78b2a55ce4dc29c6b3ec4df8ca165eba05f9b341", + "size": 613680, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.2-nightly-20140822/lwjgl-platform-2.9.2-nightly-20140822-natives-windows.jar" + } + } + }, + "extract": { + "exclude": [ + "META-INF/" + ] + }, + "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.2-nightly-20140822", + "natives": { + "linux": "natives-linux", + "osx": "natives-osx", + "windows": "natives-windows" + }, + "rules": [ + { + "action": "allow", + "os": { + "name": "osx" + } + } + ] + }, + { + "downloads": { + "classifiers": { + "natives-linux": { + "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar", + "sha1": "7ff832a6eb9ab6a767f1ade2b548092d0fa64795", + "size": 10362, + "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar" + }, + "natives-osx": { + "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-osx.jar", + "sha1": "53f9c919f34d2ca9de8c51fc4e1e8282029a9232", + "size": 12186, + "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-osx.jar" + }, + "natives-windows": { + "path": "net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-windows.jar", + "sha1": "385ee093e01f587f30ee1c8a2ee7d408fd732e16", + "size": 155179, + "url": "https://libraries.minecraft.net/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-windows.jar" + } + } + }, + "extract": { + "exclude": [ + "META-INF/" + ] + }, + "name": "net.java.jinput:jinput-platform:2.0.5", + "natives": { + "linux": "natives-linux", + "osx": "natives-osx", + "windows": "natives-windows" + } + } + ], + "mainClass": "net.minecraft.client.main.Main", + "minecraftArguments": "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} --assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} --accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}", + "minimumLauncherVersion": 18, + "releaseTime": "2016-02-29T13:49:54+00:00", + "time": "2016-03-01T13:14:53+00:00", + "type": "release" +} diff --git a/ultimmc/launcher/minecraft/testdata/codecwav-20101023.jar b/ultimmc/launcher/minecraft/testdata/codecwav-20101023.jar new file mode 100644 index 0000000..f523608 --- /dev/null +++ b/ultimmc/launcher/minecraft/testdata/codecwav-20101023.jar @@ -0,0 +1 @@ +dummy test file. diff --git a/ultimmc/launcher/minecraft/testdata/lib-native-arch.json b/ultimmc/launcher/minecraft/testdata/lib-native-arch.json new file mode 100644 index 0000000..501826a --- /dev/null +++ b/ultimmc/launcher/minecraft/testdata/lib-native-arch.json @@ -0,0 +1,46 @@ +{ + "downloads": { + "classifiers": { + "natives-osx": { + "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-osx.jar", + "sha1": "62503ee712766cf77f97252e5902786fd834b8c5", + "size": 418331, + "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-osx.jar" + }, + "natives-windows-32": { + "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar", + "sha1": "7c6affe439099806a4f552da14c42f9d643d8b23", + "size": 386792, + "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-32.jar" + }, + "natives-windows-64": { + "path": "tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar", + "sha1": "39d0c3d363735b4785598e0e7fbf8297c706a9f9", + "size": 463390, + "url": "https://libraries.minecraft.net/tv/twitch/twitch-platform/5.16/twitch-platform-5.16-natives-windows-64.jar" + } + } + }, + "extract": { + "exclude": [ + "META-INF/" + ] + }, + "name": "tv.twitch:twitch-platform:5.16", + "natives": { + "linux": "natives-linux", + "osx": "natives-osx", + "windows": "natives-windows-${arch}" + }, + "rules": [ + { + "action": "allow" + }, + { + "action": "disallow", + "os": { + "name": "linux" + } + } + ] +} diff --git a/ultimmc/launcher/minecraft/testdata/lib-native.json b/ultimmc/launcher/minecraft/testdata/lib-native.json new file mode 100644 index 0000000..5b9f3b5 --- /dev/null +++ b/ultimmc/launcher/minecraft/testdata/lib-native.json @@ -0,0 +1,52 @@ +{ + "downloads": { + "artifact": { + "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar", + "sha1": "b04f3ee8f5e43fa3b162981b50bb72fe1acabb33", + "size": 22, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209.jar" + }, + "classifiers": { + "natives-linux": { + "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar", + "sha1": "931074f46c795d2f7b30ed6395df5715cfd7675b", + "size": 578680, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-linux.jar" + }, + "natives-osx": { + "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar", + "sha1": "bcab850f8f487c3f4c4dbabde778bb82bd1a40ed", + "size": 426822, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-osx.jar" + }, + "natives-windows": { + "path": "org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar", + "sha1": "b84d5102b9dbfabfeb5e43c7e2828d98a7fc80e0", + "size": 613748, + "url": "https://libraries.minecraft.net/org/lwjgl/lwjgl/lwjgl-platform/2.9.4-nightly-20150209/lwjgl-platform-2.9.4-nightly-20150209-natives-windows.jar" + } + } + }, + "extract": { + "exclude": [ + "META-INF/" + ] + }, + "name": "org.lwjgl.lwjgl:lwjgl-platform:2.9.4-nightly-20150209", + "natives": { + "linux": "natives-linux", + "osx": "natives-osx", + "windows": "natives-windows" + }, + "rules": [ + { + "action": "allow" + }, + { + "action": "disallow", + "os": { + "name": "osx" + } + } + ] +} diff --git a/ultimmc/launcher/minecraft/testdata/lib-simple.json b/ultimmc/launcher/minecraft/testdata/lib-simple.json new file mode 100644 index 0000000..90bbff0 --- /dev/null +++ b/ultimmc/launcher/minecraft/testdata/lib-simple.json @@ -0,0 +1,11 @@ +{ + "downloads": { + "artifact": { + "path": "com/paulscode/codecwav/20101023/codecwav-20101023.jar", + "sha1": "12f031cfe88fef5c1dd36c563c0a3a69bd7261da", + "size": 5618, + "url": "https://libraries.minecraft.net/com/paulscode/codecwav/20101023/codecwav-20101023.jar" + } + }, + "name": "com.paulscode:codecwav:20101023" +} diff --git a/ultimmc/launcher/minecraft/testdata/testname-testversion-linux-32.jar b/ultimmc/launcher/minecraft/testdata/testname-testversion-linux-32.jar new file mode 100644 index 0000000..f523608 --- /dev/null +++ b/ultimmc/launcher/minecraft/testdata/testname-testversion-linux-32.jar @@ -0,0 +1 @@ +dummy test file. diff --git a/ultimmc/launcher/minecraft/update/AssetUpdateTask.cpp b/ultimmc/launcher/minecraft/update/AssetUpdateTask.cpp new file mode 100644 index 0000000..c4bddb0 --- /dev/null +++ b/ultimmc/launcher/minecraft/update/AssetUpdateTask.cpp @@ -0,0 +1,112 @@ +#include "AssetUpdateTask.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "net/ChecksumValidator.h" +#include "minecraft/AssetsUtils.h" + +#include "Application.h" + +AssetUpdateTask::AssetUpdateTask(MinecraftInstance * inst) +{ + m_inst = inst; +} + +AssetUpdateTask::~AssetUpdateTask() +{ +} + +void AssetUpdateTask::executeTask() +{ + setStatus(tr("Updating assets index...")); + auto components = m_inst->getPackProfile(); + auto profile = components->getProfile(); + auto assets = profile->getMinecraftAssets(); + QUrl indexUrl = assets->url; + QString localPath = assets->id + ".json"; + auto job = new NetJob( + tr("Asset index for %1").arg(m_inst->name()), + APPLICATION->network() + ); + + auto metacache = APPLICATION->metacache(); + auto entry = metacache->resolveEntry("asset_indexes", localPath); + entry->setStale(true); + auto hexSha1 = assets->sha1.toLatin1(); + qDebug() << "Asset index SHA1:" << hexSha1; + auto dl = Net::Download::makeCached(indexUrl, entry); + auto rawSha1 = QByteArray::fromHex(assets->sha1.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + job->addNetAction(dl); + + downloadJob.reset(job); + + connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::assetIndexFinished); + connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetIndexFailed); + connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); + + qDebug() << m_inst->name() << ": Starting asset index download"; + downloadJob->start(); +} + +bool AssetUpdateTask::canAbort() const +{ + return true; +} + +void AssetUpdateTask::assetIndexFinished() +{ + AssetsIndex index; + qDebug() << m_inst->name() << ": Finished asset index download"; + + auto components = m_inst->getPackProfile(); + auto profile = components->getProfile(); + auto assets = profile->getMinecraftAssets(); + + QString asset_fname = "assets/indexes/" + assets->id + ".json"; + // FIXME: this looks like a job for a generic validator based on json schema? + if (!AssetsUtils::loadAssetsIndexJson(assets->id, asset_fname, index)) + { + auto metacache = APPLICATION->metacache(); + auto entry = metacache->resolveEntry("asset_indexes", assets->id + ".json"); + metacache->evictEntry(entry); + emitFailed(tr("Failed to read the assets index!")); + } + + auto job = index.getDownloadJob(); + if(job) + { + setStatus(tr("Getting the assets files from Mojang...")); + downloadJob = job; + connect(downloadJob.get(), &NetJob::succeeded, this, &AssetUpdateTask::emitSucceeded); + connect(downloadJob.get(), &NetJob::failed, this, &AssetUpdateTask::assetsFailed); + connect(downloadJob.get(), &NetJob::progress, this, &AssetUpdateTask::progress); + downloadJob->start(); + return; + } + emitSucceeded(); +} + +void AssetUpdateTask::assetIndexFailed(QString reason) +{ + qDebug() << m_inst->name() << ": Failed asset index download"; + emitFailed(tr("Failed to download the assets index:\n%1").arg(reason)); +} + +void AssetUpdateTask::assetsFailed(QString reason) +{ + emitFailed(tr("Failed to download assets:\n%1").arg(reason)); +} + +bool AssetUpdateTask::abort() +{ + if(downloadJob) + { + return downloadJob->abort(); + } + else + { + qWarning() << "Prematurely aborted AssetUpdateTask"; + } + return true; +} diff --git a/ultimmc/launcher/minecraft/update/AssetUpdateTask.h b/ultimmc/launcher/minecraft/update/AssetUpdateTask.h new file mode 100644 index 0000000..6d7356f --- /dev/null +++ b/ultimmc/launcher/minecraft/update/AssetUpdateTask.h @@ -0,0 +1,28 @@ +#pragma once +#include "tasks/Task.h" +#include "net/NetJob.h" +class MinecraftInstance; + +class AssetUpdateTask : public Task +{ + Q_OBJECT +public: + AssetUpdateTask(MinecraftInstance * inst); + virtual ~AssetUpdateTask(); + + void executeTask() override; + + bool canAbort() const override; + +private slots: + void assetIndexFinished(); + void assetIndexFailed(QString reason); + void assetsFailed(QString reason); + +public slots: + bool abort() override; + +private: + MinecraftInstance *m_inst; + NetJob::Ptr downloadJob; +}; diff --git a/ultimmc/launcher/minecraft/update/FMLLibrariesTask.cpp b/ultimmc/launcher/minecraft/update/FMLLibrariesTask.cpp new file mode 100644 index 0000000..5814199 --- /dev/null +++ b/ultimmc/launcher/minecraft/update/FMLLibrariesTask.cpp @@ -0,0 +1,133 @@ +#include "FMLLibrariesTask.h" + +#include "FileSystem.h" +#include "minecraft/VersionFilterData.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +#include "BuildConfig.h" +#include "Application.h" + +FMLLibrariesTask::FMLLibrariesTask(MinecraftInstance * inst) +{ + m_inst = inst; +} +void FMLLibrariesTask::executeTask() +{ + // Get the mod list + MinecraftInstance *inst = (MinecraftInstance *)m_inst; + auto components = inst->getPackProfile(); + auto profile = components->getProfile(); + + if (!profile->hasTrait("legacyFML")) + { + emitSucceeded(); + return; + } + + QString version = components->getComponentVersion("net.minecraft"); + auto &fmlLibsMapping = g_VersionFilterData.fmlLibsMapping; + if (!fmlLibsMapping.contains(version)) + { + emitSucceeded(); + return; + } + + auto &libList = fmlLibsMapping[version]; + + // determine if we need some libs for FML or forge + setStatus(tr("Checking for FML libraries...")); + if(!components->getComponent("net.minecraftforge")) + { + emitSucceeded(); + return; + } + + // now check the lib folder inside the instance for files. + for (auto &lib : libList) + { + QFileInfo libInfo(FS::PathCombine(inst->libDir(), lib.filename)); + if (libInfo.exists()) + continue; + fmlLibsToProcess.append(lib); + } + + // if everything is in place, there's nothing to do here... + if (fmlLibsToProcess.isEmpty()) + { + emitSucceeded(); + return; + } + + // download missing libs to our place + setStatus(tr("Downloading FML libraries...")); + auto dljob = new NetJob("FML libraries", APPLICATION->network()); + auto metacache = APPLICATION->metacache(); + for (auto &lib : fmlLibsToProcess) + { + auto entry = metacache->resolveEntry("fmllibs", lib.filename); + QString urlString = BuildConfig.FMLLIBS_BASE_URL + lib.filename; + dljob->addNetAction(Net::Download::makeCached(QUrl(urlString), entry)); + } + + connect(dljob, &NetJob::succeeded, this, &FMLLibrariesTask::fmllibsFinished); + connect(dljob, &NetJob::failed, this, &FMLLibrariesTask::fmllibsFailed); + connect(dljob, &NetJob::progress, this, &FMLLibrariesTask::progress); + downloadJob.reset(dljob); + downloadJob->start(); +} + +bool FMLLibrariesTask::canAbort() const +{ + return true; +} + +void FMLLibrariesTask::fmllibsFinished() +{ + downloadJob.reset(); + if (!fmlLibsToProcess.isEmpty()) + { + setStatus(tr("Copying FML libraries into the instance...")); + MinecraftInstance *inst = (MinecraftInstance *)m_inst; + auto metacache = APPLICATION->metacache(); + int index = 0; + for (auto &lib : fmlLibsToProcess) + { + progress(index, fmlLibsToProcess.size()); + auto entry = metacache->resolveEntry("fmllibs", lib.filename); + auto path = FS::PathCombine(inst->libDir(), lib.filename); + if (!FS::ensureFilePathExists(path)) + { + emitFailed(tr("Failed creating FML library folder inside the instance.")); + return; + } + if (!QFile::copy(entry->getFullPath(), FS::PathCombine(inst->libDir(), lib.filename))) + { + emitFailed(tr("Failed copying Forge/FML library: %1.").arg(lib.filename)); + return; + } + index++; + } + progress(index, fmlLibsToProcess.size()); + } + emitSucceeded(); +} +void FMLLibrariesTask::fmllibsFailed(QString reason) +{ + QStringList failed = downloadJob->getFailedFiles(); + QString failed_all = failed.join("\n"); + emitFailed(tr("Failed to download the following files:\n%1\n\nReason:%2\nPlease try again.").arg(failed_all, reason)); +} + +bool FMLLibrariesTask::abort() +{ + if(downloadJob) + { + return downloadJob->abort(); + } + else + { + qWarning() << "Prematurely aborted FMLLibrariesTask"; + } + return true; +} diff --git a/ultimmc/launcher/minecraft/update/FMLLibrariesTask.h b/ultimmc/launcher/minecraft/update/FMLLibrariesTask.h new file mode 100644 index 0000000..2e5ad83 --- /dev/null +++ b/ultimmc/launcher/minecraft/update/FMLLibrariesTask.h @@ -0,0 +1,31 @@ +#pragma once +#include "tasks/Task.h" +#include "net/NetJob.h" +#include "minecraft/VersionFilterData.h" + +class MinecraftInstance; + +class FMLLibrariesTask : public Task +{ + Q_OBJECT +public: + FMLLibrariesTask(MinecraftInstance * inst); + virtual ~FMLLibrariesTask() {}; + + void executeTask() override; + + bool canAbort() const override; + +private slots: + void fmllibsFinished(); + void fmllibsFailed(QString reason); + +public slots: + bool abort() override; + +private: + MinecraftInstance *m_inst; + NetJob::Ptr downloadJob; + QList fmlLibsToProcess; +}; + diff --git a/ultimmc/launcher/minecraft/update/FoldersTask.cpp b/ultimmc/launcher/minecraft/update/FoldersTask.cpp new file mode 100644 index 0000000..e2b1bb4 --- /dev/null +++ b/ultimmc/launcher/minecraft/update/FoldersTask.cpp @@ -0,0 +1,21 @@ +#include "FoldersTask.h" +#include "minecraft/MinecraftInstance.h" +#include + +FoldersTask::FoldersTask(MinecraftInstance * inst) + :Task() +{ + m_inst = inst; +} + +void FoldersTask::executeTask() +{ + // Make directories + QDir mcDir(m_inst->gameRoot()); + if (!mcDir.exists() && !mcDir.mkpath(".")) + { + emitFailed(tr("Failed to create folder for minecraft binaries.")); + return; + } + emitSucceeded(); +} diff --git a/ultimmc/launcher/minecraft/update/FoldersTask.h b/ultimmc/launcher/minecraft/update/FoldersTask.h new file mode 100644 index 0000000..f6ed5e6 --- /dev/null +++ b/ultimmc/launcher/minecraft/update/FoldersTask.h @@ -0,0 +1,17 @@ +#pragma once + +#include "tasks/Task.h" + +class MinecraftInstance; +class FoldersTask : public Task +{ + Q_OBJECT +public: + FoldersTask(MinecraftInstance * inst); + virtual ~FoldersTask() {}; + + void executeTask() override; +private: + MinecraftInstance *m_inst; +}; + diff --git a/ultimmc/launcher/minecraft/update/LibrariesTask.cpp b/ultimmc/launcher/minecraft/update/LibrariesTask.cpp new file mode 100644 index 0000000..667dd5d --- /dev/null +++ b/ultimmc/launcher/minecraft/update/LibrariesTask.cpp @@ -0,0 +1,92 @@ +#include "LibrariesTask.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" + +#include "Application.h" + +LibrariesTask::LibrariesTask(MinecraftInstance * inst) +{ + m_inst = inst; +} + +void LibrariesTask::executeTask() +{ + setStatus(tr("Getting the library files from Mojang...")); + qDebug() << m_inst->name() << ": downloading libraries"; + MinecraftInstance *inst = (MinecraftInstance *)m_inst; + + // Build a list of URLs that will need to be downloaded. + auto components = inst->getPackProfile(); + auto profile = components->getProfile(); + + auto job = new NetJob(tr("Libraries for instance %1").arg(inst->name()), APPLICATION->network()); + downloadJob.reset(job); + + auto metacache = APPLICATION->metacache(); + + auto processArtifactPool = [&](const QList & pool, QStringList & errors, const QString & localPath) + { + for (auto lib : pool) + { + if(!lib) + { + emitFailed(tr("Null jar is specified in the metadata, aborting.")); + return false; + } + auto dls = lib->getDownloads(currentSystem, metacache.get(), errors, localPath); + for(auto dl : dls) + { + downloadJob->addNetAction(dl); + } + } + return true; + }; + + QStringList failedLocalLibraries; + QList libArtifactPool; + libArtifactPool.append(profile->getLibraries()); + libArtifactPool.append(profile->getNativeLibraries()); + libArtifactPool.append(profile->getMavenFiles()); + libArtifactPool.append(profile->getMainJar()); + processArtifactPool(libArtifactPool, failedLocalLibraries, inst->getLocalLibraryPath()); + + QStringList failedLocalJarMods; + processArtifactPool(profile->getJarMods(), failedLocalJarMods, inst->jarModsDir()); + + if (!failedLocalJarMods.empty() || !failedLocalLibraries.empty()) + { + downloadJob.reset(); + QString failed_all = (failedLocalLibraries + failedLocalJarMods).join("\n"); + emitFailed(tr("Some artifacts marked as 'local' are missing their files:\n%1\n\nYou need to either add the files, or removed the packages that require them.\nYou'll have to correct this problem manually.").arg(failed_all)); + return; + } + + connect(downloadJob.get(), &NetJob::succeeded, this, &LibrariesTask::emitSucceeded); + connect(downloadJob.get(), &NetJob::failed, this, &LibrariesTask::jarlibFailed); + connect(downloadJob.get(), &NetJob::progress, this, &LibrariesTask::progress); + downloadJob->start(); +} + +bool LibrariesTask::canAbort() const +{ + return true; +} + +void LibrariesTask::jarlibFailed(QString reason) +{ + emitFailed(tr("Game update failed: it was impossible to fetch the required libraries.\nReason:\n%1").arg(reason)); +} + +bool LibrariesTask::abort() +{ + if(downloadJob) + { + return downloadJob->abort(); + } + else + { + qWarning() << "Prematurely aborted LibrariesTask"; + } + return true; +} diff --git a/ultimmc/launcher/minecraft/update/LibrariesTask.h b/ultimmc/launcher/minecraft/update/LibrariesTask.h new file mode 100644 index 0000000..b966ad6 --- /dev/null +++ b/ultimmc/launcher/minecraft/update/LibrariesTask.h @@ -0,0 +1,26 @@ +#pragma once +#include "tasks/Task.h" +#include "net/NetJob.h" +class MinecraftInstance; + +class LibrariesTask : public Task +{ + Q_OBJECT +public: + LibrariesTask(MinecraftInstance * inst); + virtual ~LibrariesTask() {}; + + void executeTask() override; + + bool canAbort() const override; + +private slots: + void jarlibFailed(QString reason); + +public slots: + bool abort() override; + +private: + MinecraftInstance *m_inst; + NetJob::Ptr downloadJob; +}; diff --git a/ultimmc/launcher/modplatform/atlauncher/ATLPackIndex.cpp b/ultimmc/launcher/modplatform/atlauncher/ATLPackIndex.cpp new file mode 100644 index 0000000..0e7bc8b --- /dev/null +++ b/ultimmc/launcher/modplatform/atlauncher/ATLPackIndex.cpp @@ -0,0 +1,50 @@ +/* + * Copyright 2020-2021 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ATLPackIndex.h" + +#include + +#include "Json.h" + +static void loadIndexedVersion(ATLauncher::IndexedVersion & v, QJsonObject & obj) +{ + v.version = Json::requireString(obj, "version"); + v.minecraft = Json::requireString(obj, "minecraft"); +} + +void ATLauncher::loadIndexedPack(ATLauncher::IndexedPack & m, QJsonObject & obj) +{ + m.id = Json::requireInteger(obj, "id"); + m.position = Json::requireInteger(obj, "position"); + m.name = Json::requireString(obj, "name"); + m.type = Json::requireString(obj, "type") == "private" ? + ATLauncher::PackType::Private : + ATLauncher::PackType::Public; + auto versionsArr = Json::requireArray(obj, "versions"); + for (const auto versionRaw : versionsArr) + { + auto versionObj = Json::requireValueObject(versionRaw); + ATLauncher::IndexedVersion version; + loadIndexedVersion(version, versionObj); + m.versions.append(version); + } + m.system = Json::ensureBoolean(obj, QString("system"), false); + m.description = Json::ensureString(obj, "description", ""); + + m.safeName = Json::requireString(obj, "name").replace(QRegularExpression("[^A-Za-z0-9]"), ""); +} diff --git a/ultimmc/launcher/modplatform/atlauncher/ATLPackIndex.h b/ultimmc/launcher/modplatform/atlauncher/ATLPackIndex.h new file mode 100644 index 0000000..337b80b --- /dev/null +++ b/ultimmc/launcher/modplatform/atlauncher/ATLPackIndex.h @@ -0,0 +1,50 @@ +/* + * Copyright 2020-2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "ATLPackManifest.h" + +#include +#include +#include + +namespace ATLauncher +{ + +struct IndexedVersion +{ + QString version; + QString minecraft; +}; + +struct IndexedPack +{ + int id; + int position; + QString name; + PackType type; + QVector versions; + bool system; + QString description; + + QString safeName; +}; + +void loadIndexedPack(IndexedPack & m, QJsonObject & obj); +} + +Q_DECLARE_METATYPE(ATLauncher::IndexedPack) diff --git a/ultimmc/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp b/ultimmc/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp new file mode 100644 index 0000000..c9dc12c --- /dev/null +++ b/ultimmc/launcher/modplatform/atlauncher/ATLPackInstallTask.cpp @@ -0,0 +1,825 @@ +/* + * Copyright 2020-2022 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ATLPackInstallTask.h" + +#include + +#include + +#include "MMCZip.h" +#include "minecraft/OneSixVersionFormat.h" +#include "Version.h" +#include "net/ChecksumValidator.h" +#include "FileSystem.h" +#include "Json.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "settings/INISettingsObject.h" +#include "meta/Index.h" +#include "meta/Version.h" +#include "meta/VersionList.h" + +#include "BuildConfig.h" +#include "Application.h" + +namespace ATLauncher { + +PackInstallTask::PackInstallTask(UserInteractionSupport *support, QString packName, QString version) +{ + m_support = support; + m_pack_name = packName; + m_pack_safe_name = packName.replace(QRegularExpression("[^A-Za-z0-9]"), ""); + m_version_name = version; +} + +bool PackInstallTask::abort() +{ + if(abortable) + { + return jobPtr->abort(); + } + return false; +} + +void PackInstallTask::executeTask() +{ + qDebug() << "PackInstallTask::executeTask: " << QThread::currentThreadId(); + auto *netJob = new NetJob("ATLauncher::VersionFetch", APPLICATION->network()); + auto searchUrl = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.json") + .arg(m_pack_safe_name).arg(m_version_name); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(); + + QObject::connect(netJob, &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); + QObject::connect(netJob, &NetJob::failed, this, &PackInstallTask::onDownloadFailed); +} + +void PackInstallTask::onDownloadSucceeded() +{ + qDebug() << "PackInstallTask::onDownloadSucceeded: " << QThread::currentThreadId(); + jobPtr.reset(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from FTB at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + auto obj = doc.object(); + + ATLauncher::PackVersion version; + try + { + ATLauncher::loadVersion(version, obj); + } + catch (const JSONValidationError &e) + { + emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); + return; + } + m_version = version; + + // Display install message if one exists + if (!m_version.messages.install.isEmpty()) + m_support->displayMessage(m_version.messages.install); + + auto vlist = APPLICATION->metadataIndex()->get("net.minecraft"); + if(!vlist) + { + emitFailed(tr("Failed to get local metadata index for %1").arg("net.minecraft")); + return; + } + + auto ver = vlist->getVersion(m_version.minecraft); + if (!ver) { + emitFailed(tr("Failed to get local metadata index for '%1' v%2").arg("net.minecraft").arg(m_version.minecraft)); + return; + } + ver->load(Net::Mode::Online); + minecraftVersion = ver; + + if(m_version.noConfigs) { + downloadMods(); + } + else { + installConfigs(); + } +} + +void PackInstallTask::onDownloadFailed(QString reason) +{ + qDebug() << "PackInstallTask::onDownloadFailed: " << QThread::currentThreadId(); + jobPtr.reset(); + emitFailed(reason); +} + +QString PackInstallTask::getDirForModType(ModType type, QString raw) +{ + switch (type) { + // Mod types that can either be ignored at this stage, or ignored + // completely. + case ModType::Root: + case ModType::Extract: + case ModType::Decomp: + case ModType::TexturePackExtract: + case ModType::ResourcePackExtract: + case ModType::MCPC: + return Q_NULLPTR; + case ModType::Forge: + // Forge detection happens later on, if it cannot be detected it will + // install a jarmod component. + case ModType::Jar: + return "jarmods"; + case ModType::Mods: + return "mods"; + case ModType::Flan: + return "Flan"; + case ModType::Dependency: + return FS::PathCombine("mods", m_version.minecraft); + case ModType::Ic2Lib: + return FS::PathCombine("mods", "ic2"); + case ModType::DenLib: + return FS::PathCombine("mods", "denlib"); + case ModType::Coremods: + return "coremods"; + case ModType::Plugins: + return "plugins"; + case ModType::TexturePack: + return "texturepacks"; + case ModType::ResourcePack: + return "resourcepacks"; + case ModType::ShaderPack: + return "shaderpacks"; + case ModType::Millenaire: + qWarning() << "Unsupported mod type: " + raw; + return Q_NULLPTR; + case ModType::Unknown: + emitFailed(tr("Unknown mod type: %1").arg(raw)); + return Q_NULLPTR; + } + + return Q_NULLPTR; +} + +QString PackInstallTask::getVersionForLoader(QString uid) +{ + if(m_version.loader.recommended || m_version.loader.latest || m_version.loader.choose) { + auto vlist = APPLICATION->metadataIndex()->get(uid); + if(!vlist) + { + emitFailed(tr("Failed to get local metadata index for %1").arg(uid)); + return Q_NULLPTR; + } + + if(!vlist->isLoaded()) { + vlist->load(Net::Mode::Online); + } + + if(m_version.loader.recommended || m_version.loader.latest) { + for (int i = 0; i < vlist->versions().size(); i++) { + auto version = vlist->versions().at(i); + auto reqs = version->depends(); + + // filter by minecraft version, if the loader depends on a certain version. + // not all mod loaders depend on a given Minecraft version, so we won't do this + // filtering for those loaders. + if (m_version.loader.type != "fabric") { + auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require &req) { + return req.uid == "net.minecraft"; + }); + if (iter == reqs.end()) continue; + if (iter->equalsVersion != m_version.minecraft) continue; + } + + if (m_version.loader.recommended) { + // first recommended build we find, we use. + if (!version->isRecommended()) continue; + } + + return version->descriptor(); + } + + emitFailed(tr("Failed to find version for %1 loader").arg(m_version.loader.type)); + return Q_NULLPTR; + } + else if(m_version.loader.choose) { + // Fabric Loader doesn't depend on a given Minecraft version. + if (m_version.loader.type == "fabric") { + return m_support->chooseVersion(vlist, Q_NULLPTR); + } + + return m_support->chooseVersion(vlist, m_version.minecraft); + } + } + + if (m_version.loader.version == Q_NULLPTR || m_version.loader.version.isEmpty()) { + emitFailed(tr("No loader version set for modpack!")); + return Q_NULLPTR; + } + + return m_version.loader.version; +} + +QString PackInstallTask::detectLibrary(VersionLibrary library) +{ + // Try to detect what the library is + if (!library.server.isEmpty() && library.server.split("/").length() >= 3) { + auto lastSlash = library.server.lastIndexOf("/"); + auto locationAndVersion = library.server.mid(0, lastSlash); + auto fileName = library.server.mid(lastSlash + 1); + + lastSlash = locationAndVersion.lastIndexOf("/"); + auto location = locationAndVersion.mid(0, lastSlash); + auto version = locationAndVersion.mid(lastSlash + 1); + + lastSlash = location.lastIndexOf("/"); + auto group = location.mid(0, lastSlash).replace("/", "."); + auto artefact = location.mid(lastSlash + 1); + + return group + ":" + artefact + ":" + version; + } + + if(library.file.contains("-")) { + auto lastSlash = library.file.lastIndexOf("-"); + auto name = library.file.mid(0, lastSlash); + auto version = library.file.mid(lastSlash + 1).remove(".jar"); + + if(name == QString("guava")) { + return "com.google.guava:guava:" + version; + } + else if(name == QString("commons-lang3")) { + return "org.apache.commons:commons-lang3:" + version; + } + } + + return "org.multimc.atlauncher:" + library.md5 + ":1"; +} + +bool PackInstallTask::createLibrariesComponent(QString instanceRoot, std::shared_ptr profile) +{ + if(m_version.libraries.isEmpty()) { + return true; + } + + QList exempt; + for(const auto & componentUid : componentsToInstall.keys()) { + auto componentVersion = componentsToInstall.value(componentUid); + + for(const auto & library : componentVersion->data()->libraries) { + GradleSpecifier lib(library->rawName()); + exempt.append(lib); + } + } + + { + for(const auto & library : minecraftVersion->data()->libraries) { + GradleSpecifier lib(library->rawName()); + exempt.append(lib); + } + } + + auto uuid = QUuid::createUuid(); + auto id = uuid.toString().remove('{').remove('}'); + auto target_id = "org.multimc.atlauncher." + id; + + auto patchDir = FS::PathCombine(instanceRoot, "patches"); + if(!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + + auto f = std::make_shared(); + f->name = m_pack_name + " " + m_version_name + " (libraries)"; + + for(const auto & lib : m_version.libraries) { + auto libName = detectLibrary(lib); + GradleSpecifier libSpecifier(libName); + + bool libExempt = false; + for(const auto & existingLib : exempt) { + if(libSpecifier.matchName(existingLib)) { + // If the pack specifies a newer version of the lib, use that! + libExempt = Version(libSpecifier.version()) >= Version(existingLib.version()); + } + } + if(libExempt) continue; + + auto library = std::make_shared(); + library->setRawName(libName); + + switch(lib.download) { + case DownloadType::Server: + library->setAbsoluteUrl(BuildConfig.ATL_DOWNLOAD_SERVER_URL + lib.url); + break; + case DownloadType::Direct: + library->setAbsoluteUrl(lib.url); + break; + case DownloadType::Browser: + case DownloadType::Unknown: + emitFailed(tr("Unknown or unsupported download type: %1").arg(lib.download_raw)); + return false; + } + + f->libraries.append(library); + } + + if(f->libraries.isEmpty()) { + return true; + } + + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + profile->appendComponent(new Component(profile.get(), target_id, f)); + return true; +} + +bool PackInstallTask::createPackComponent(QString instanceRoot, std::shared_ptr profile) +{ + if(m_version.mainClass.mainClass.isEmpty() && m_version.extraArguments.arguments.isEmpty()) { + return true; + } + + auto mainClass = m_version.mainClass.mainClass; + auto extraArguments = m_version.extraArguments.arguments; + + auto hasMainClassDepends = !m_version.mainClass.depends.isEmpty(); + auto hasExtraArgumentsDepends = !m_version.extraArguments.depends.isEmpty(); + if (hasMainClassDepends || hasExtraArgumentsDepends) { + QSet mods; + for (const auto& item : m_version.mods) { + mods.insert(item.name); + } + + if (hasMainClassDepends && !mods.contains(m_version.mainClass.depends)) { + mainClass = ""; + } + + if (hasExtraArgumentsDepends && !mods.contains(m_version.extraArguments.depends)) { + extraArguments = ""; + } + } + + if (mainClass.isEmpty() && extraArguments.isEmpty()) { + return true; + } + + auto uuid = QUuid::createUuid(); + auto id = uuid.toString().remove('{').remove('}'); + auto target_id = "org.multimc.atlauncher." + id; + + auto patchDir = FS::PathCombine(instanceRoot, "patches"); + if(!FS::ensureFolderPathExists(patchDir)) + { + return false; + } + auto patchFileName = FS::PathCombine(patchDir, target_id + ".json"); + + QStringList mainClasses; + QStringList tweakers; + for(const auto & componentUid : componentsToInstall.keys()) { + auto componentVersion = componentsToInstall.value(componentUid); + + if(componentVersion->data()->mainClass != QString("")) { + mainClasses.append(componentVersion->data()->mainClass); + } + tweakers.append(componentVersion->data()->addTweakers); + } + + auto f = std::make_shared(); + f->name = m_pack_name + " " + m_version_name; + if(!mainClass.isEmpty() && !mainClasses.contains(mainClass)) { + f->mainClass = mainClass; + } + + // Parse out tweakers + auto args = extraArguments.split(" "); + QString previous; + for(auto arg : args) { + if(arg.startsWith("--tweakClass=") || previous == "--tweakClass") { + auto tweakClass = arg.remove("--tweakClass="); + if(tweakers.contains(tweakClass)) continue; + + f->addTweakers.append(tweakClass); + } + previous = arg; + } + + if(f->mainClass == QString() && f->addTweakers.isEmpty()) { + return true; + } + + QFile file(patchFileName); + if (!file.open(QFile::WriteOnly)) + { + qCritical() << "Error opening" << file.fileName() + << "for reading:" << file.errorString(); + return false; + } + file.write(OneSixVersionFormat::versionFileToJson(f).toJson()); + file.close(); + + profile->appendComponent(new Component(profile.get(), target_id, f)); + return true; +} + +void PackInstallTask::installConfigs() +{ + qDebug() << "PackInstallTask::installConfigs: " << QThread::currentThreadId(); + setStatus(tr("Downloading configs...")); + jobPtr = new NetJob(tr("Config download"), APPLICATION->network()); + + auto path = QString("Configs/%1/%2.zip").arg(m_pack_safe_name).arg(m_version_name); + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "packs/%1/versions/%2/Configs.zip") + .arg(m_pack_safe_name).arg(m_version_name); + auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", path); + entry->setStale(true); + + auto dl = Net::Download::makeCached(url, entry); + if (!m_version.configs.sha1.isEmpty()) { + auto rawSha1 = QByteArray::fromHex(m_version.configs.sha1.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawSha1)); + } + jobPtr->addNetAction(dl); + archivePath = entry->getFullPath(); + + connect(jobPtr.get(), &NetJob::succeeded, this, [&]() + { + abortable = false; + jobPtr.reset(); + extractConfigs(); + }); + connect(jobPtr.get(), &NetJob::failed, [&](QString reason) + { + abortable = false; + jobPtr.reset(); + emitFailed(reason); + }); + connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) + { + abortable = true; + setProgress(current, total); + }); + + jobPtr->start(); +} + +void PackInstallTask::extractConfigs() +{ + qDebug() << "PackInstallTask::extractConfigs: " << QThread::currentThreadId(); + setStatus(tr("Extracting configs...")); + + QDir extractDir(m_stagingPath); + + QuaZip packZip(archivePath); + if(!packZip.open(QuaZip::mdUnzip)) + { + emitFailed(tr("Failed to open pack configs %1!").arg(archivePath)); + return; + } + + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/minecraft"); + connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, [&]() + { + downloadMods(); + }); + connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, [&]() + { + emitAborted(); + }); + m_extractFutureWatcher.setFuture(m_extractFuture); +} + +void PackInstallTask::downloadMods() +{ + qDebug() << "PackInstallTask::installMods: " << QThread::currentThreadId(); + + QVector optionalMods; + for (const auto& mod : m_version.mods) { + if (mod.optional) { + optionalMods.push_back(mod); + } + } + + // Select optional mods, if pack contains any + QVector selectedMods; + if (!optionalMods.isEmpty()) { + setStatus(tr("Selecting optional mods...")); + selectedMods = m_support->chooseOptionalMods(m_version, optionalMods); + } + + setStatus(tr("Downloading mods...")); + + jarmods.clear(); + jobPtr = new NetJob(tr("Mod download"), APPLICATION->network()); + for(const auto& mod : m_version.mods) { + // skip non-client mods + if(!mod.client) continue; + + // skip optional mods that were not selected + if(mod.optional && !selectedMods.contains(mod.name)) continue; + + QString url; + switch(mod.download) { + case DownloadType::Server: + url = BuildConfig.ATL_DOWNLOAD_SERVER_URL + mod.url; + break; + case DownloadType::Browser: + emitFailed(tr("Unsupported download type: %1").arg(mod.download_raw)); + return; + case DownloadType::Direct: + url = mod.url; + break; + case DownloadType::Unknown: + emitFailed(tr("Unknown download type: %1").arg(mod.download_raw)); + return; + } + + QFileInfo fileName(mod.file); + auto cacheName = fileName.completeBaseName() + "-" + mod.md5 + "." + fileName.suffix(); + + if (mod.type == ModType::Extract || mod.type == ModType::TexturePackExtract || mod.type == ModType::ResourcePackExtract) { + auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); + entry->setStale(true); + modsToExtract.insert(entry->getFullPath(), mod); + + auto dl = Net::Download::makeCached(url, entry); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } + jobPtr->addNetAction(dl); + } + else if(mod.type == ModType::Decomp) { + auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); + entry->setStale(true); + modsToDecomp.insert(entry->getFullPath(), mod); + + auto dl = Net::Download::makeCached(url, entry); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } + jobPtr->addNetAction(dl); + } + else { + auto relpath = getDirForModType(mod.type, mod.type_raw); + if(relpath == Q_NULLPTR) continue; + + auto entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", cacheName); + entry->setStale(true); + + auto dl = Net::Download::makeCached(url, entry); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } + jobPtr->addNetAction(dl); + + auto path = FS::PathCombine(m_stagingPath, "minecraft", relpath, mod.file); + qDebug() << "Will download" << url << "to" << path; + modsToCopy[entry->getFullPath()] = path; + + if(mod.type == ModType::Forge) { + auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge"); + if(vlist) + { + auto ver = vlist->getVersion(mod.version); + if(ver) { + ver->load(Net::Mode::Online); + componentsToInstall.insert("net.minecraftforge", ver); + continue; + } + } + + qDebug() << "Jarmod: " + path; + jarmods.push_back(path); + } + + if(mod.type == ModType::Jar) { + qDebug() << "Jarmod: " + path; + jarmods.push_back(path); + } + } + } + + connect(jobPtr.get(), &NetJob::succeeded, this, &PackInstallTask::onModsDownloaded); + connect(jobPtr.get(), &NetJob::failed, [&](QString reason) + { + abortable = false; + jobPtr.reset(); + emitFailed(reason); + }); + connect(jobPtr.get(), &NetJob::progress, [&](qint64 current, qint64 total) + { + abortable = true; + setProgress(current, total); + }); + + jobPtr->start(); +} + +void PackInstallTask::onModsDownloaded() { + abortable = false; + + qDebug() << "PackInstallTask::onModsDownloaded: " << QThread::currentThreadId(); + jobPtr.reset(); + + if(!modsToExtract.empty() || !modsToDecomp.empty() || !modsToCopy.empty()) { + m_modExtractFuture = QtConcurrent::run(QThreadPool::globalInstance(), this, &PackInstallTask::extractMods, modsToExtract, modsToDecomp, modsToCopy); + connect(&m_modExtractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onModsExtracted); + connect(&m_modExtractFutureWatcher, &QFutureWatcher::canceled, this, [&]() + { + emitAborted(); + }); + m_modExtractFutureWatcher.setFuture(m_modExtractFuture); + } + else { + install(); + } +} + +void PackInstallTask::onModsExtracted() { + qDebug() << "PackInstallTask::onModsExtracted: " << QThread::currentThreadId(); + if(m_modExtractFuture.result()) { + install(); + } + else { + emitFailed(tr("Failed to extract mods...")); + } +} + +bool PackInstallTask::extractMods( + const QMap &toExtract, + const QMap &toDecomp, + const QMap &toCopy +) { + qDebug() << "PackInstallTask::extractMods: " << QThread::currentThreadId(); + + setStatus(tr("Extracting mods...")); + for (auto iter = toExtract.begin(); iter != toExtract.end(); iter++) { + auto &modPath = iter.key(); + auto &mod = iter.value(); + + QString extractToDir; + if(mod.type == ModType::Extract) { + extractToDir = getDirForModType(mod.extractTo, mod.extractTo_raw); + } + else if(mod.type == ModType::TexturePackExtract) { + extractToDir = FS::PathCombine("texturepacks", "extracted"); + } + else if(mod.type == ModType::ResourcePackExtract) { + extractToDir = FS::PathCombine("resourcepacks", "extracted"); + } + + QDir extractDir(m_stagingPath); + auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir); + + QString folderToExtract = ""; + if(mod.type == ModType::Extract) { + folderToExtract = mod.extractFolder; + folderToExtract.remove(QRegExp("^/")); + } + + qDebug() << "Extracting " + mod.file + " to " + extractToDir; + if(!MMCZip::extractDir(modPath, folderToExtract, extractToPath)) { + // assume error + return false; + } + } + + for (auto iter = toDecomp.begin(); iter != toDecomp.end(); iter++) { + auto &modPath = iter.key(); + auto &mod = iter.value(); + auto extractToDir = getDirForModType(mod.decompType, mod.decompType_raw); + + QDir extractDir(m_stagingPath); + auto extractToPath = FS::PathCombine(extractDir.absolutePath(), "minecraft", extractToDir, mod.decompFile); + + qDebug() << "Extracting " + mod.decompFile + " to " + extractToDir; + if(!MMCZip::extractFile(modPath, mod.decompFile, extractToPath)) { + qWarning() << "Failed to extract" << mod.decompFile; + return false; + } + } + + for (auto iter = toCopy.begin(); iter != toCopy.end(); iter++) { + auto &from = iter.key(); + auto &to = iter.value(); + + // If the file already exists, assume the mod is the correct copy - and remove + // the copy from the Configs.zip + QFileInfo fileInfo(to); + if (fileInfo.exists()) { + if (!QFile::remove(to)) { + qWarning() << "Failed to delete" << to; + return false; + } + } + + FS::copy fileCopyOperation(from, to); + if(!fileCopyOperation()) { + qWarning() << "Failed to copy" << from << "to" << to; + return false; + } + } + return true; +} + +void PackInstallTask::install() +{ + qDebug() << "PackInstallTask::install: " << QThread::currentThreadId(); + setStatus(tr("Installing modpack")); + + auto instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(instanceConfigPath); + instanceSettings->suspendSave(); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + + // Use a component to add libraries BEFORE Minecraft + if(!createLibrariesComponent(instance.instanceRoot(), components)) { + emitFailed(tr("Failed to create libraries component")); + return; + } + + // Minecraft + components->setComponentVersion("net.minecraft", m_version.minecraft, true); + + // Loader + if(m_version.loader.type == QString("forge")) + { + auto version = getVersionForLoader("net.minecraftforge"); + if(version == Q_NULLPTR) return; + + components->setComponentVersion("net.minecraftforge", version, true); + } + else if(m_version.loader.type == QString("fabric")) + { + auto version = getVersionForLoader("net.fabricmc.fabric-loader"); + if(version == Q_NULLPTR) return; + + components->setComponentVersion("net.fabricmc.fabric-loader", version, true); + } + else if(m_version.loader.type != QString()) + { + emitFailed(tr("Unknown loader type: ") + m_version.loader.type); + return; + } + + for(const auto & componentUid : componentsToInstall.keys()) { + auto version = componentsToInstall.value(componentUid); + components->setComponentVersion(componentUid, version->version()); + } + + components->installJarMods(jarmods); + + // Use a component to fill in the rest of the data + // todo: use more detection + if(!createPackComponent(instance.instanceRoot(), components)) { + emitFailed(tr("Failed to create pack component")); + return; + } + + components->saveNow(); + + instance.setName(m_instName); + instance.setIconKey(m_instIcon); + instance.setManagedPack("atlauncher", m_pack_safe_name, m_pack_name, m_version_name, m_version_name); + instanceSettings->resumeSave(); + + jarmods.clear(); + emitSucceeded(); +} + +} diff --git a/ultimmc/launcher/modplatform/atlauncher/ATLPackInstallTask.h b/ultimmc/launcher/modplatform/atlauncher/ATLPackInstallTask.h new file mode 100644 index 0000000..44bf01e --- /dev/null +++ b/ultimmc/launcher/modplatform/atlauncher/ATLPackInstallTask.h @@ -0,0 +1,124 @@ +/* + * Copyright 2020-2022 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "ATLPackManifest.h" + +#include "InstanceTask.h" +#include "net/NetJob.h" +#include "settings/INISettingsObject.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "meta/Version.h" + +#include + +namespace ATLauncher { + +class UserInteractionSupport { + +public: + /** + * Requests a user interaction to select which optional mods should be installed. + */ + virtual QVector chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) = 0; + + /** + * Requests a user interaction to select a component version from a given version list + * and constrained to a given Minecraft version. + */ + virtual QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) = 0; + + /** + * Requests a user interaction to display a message. + */ + virtual void displayMessage(QString message) = 0; + +}; + +class PackInstallTask : public InstanceTask +{ +Q_OBJECT + +public: + explicit PackInstallTask(UserInteractionSupport *support, QString packName, QString version); + virtual ~PackInstallTask(){} + + bool canAbort() const override { return true; } + bool abort() override; + +protected: + virtual void executeTask() override; + +private slots: + void onDownloadSucceeded(); + void onDownloadFailed(QString reason); + + void onModsDownloaded(); + void onModsExtracted(); + +private: + QString getDirForModType(ModType type, QString raw); + QString getVersionForLoader(QString uid); + QString detectLibrary(VersionLibrary library); + + bool createLibrariesComponent(QString instanceRoot, std::shared_ptr profile); + bool createPackComponent(QString instanceRoot, std::shared_ptr profile); + + void installConfigs(); + void extractConfigs(); + void downloadMods(); + bool extractMods( + const QMap &toExtract, + const QMap &toDecomp, + const QMap &toCopy + ); + void install(); + +private: + UserInteractionSupport *m_support; + + bool abortable = false; + + NetJob::Ptr jobPtr; + QByteArray response; + + QString m_pack_name; + QString m_pack_safe_name; + QString m_version_name; + PackVersion m_version; + + QMap modsToExtract; + QMap modsToDecomp; + QMap modsToCopy; + + QString archivePath; + QStringList jarmods; + Meta::VersionPtr minecraftVersion; + QMap componentsToInstall; + + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; + + QFuture m_modExtractFuture; + QFutureWatcher m_modExtractFutureWatcher; + +}; + +} diff --git a/ultimmc/launcher/modplatform/atlauncher/ATLPackManifest.cpp b/ultimmc/launcher/modplatform/atlauncher/ATLPackManifest.cpp new file mode 100644 index 0000000..f8f7c2f --- /dev/null +++ b/ultimmc/launcher/modplatform/atlauncher/ATLPackManifest.cpp @@ -0,0 +1,276 @@ +/* + * Copyright 2020-2022 Jamie Mansfield + * Copyright 2021 Petr Mrazek + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ATLPackManifest.h" + +#include "Json.h" + +static ATLauncher::DownloadType parseDownloadType(QString rawType) { + if(rawType == QString("server")) { + return ATLauncher::DownloadType::Server; + } + else if(rawType == QString("browser")) { + return ATLauncher::DownloadType::Browser; + } + else if(rawType == QString("direct")) { + return ATLauncher::DownloadType::Direct; + } + + return ATLauncher::DownloadType::Unknown; +} + +static ATLauncher::ModType parseModType(QString rawType) { + // See https://wiki.atlauncher.com/mod_types + if(rawType == QString("root")) { + return ATLauncher::ModType::Root; + } + else if(rawType == QString("forge")) { + return ATLauncher::ModType::Forge; + } + else if(rawType == QString("jar")) { + return ATLauncher::ModType::Jar; + } + else if(rawType == QString("mods")) { + return ATLauncher::ModType::Mods; + } + else if(rawType == QString("flan")) { + return ATLauncher::ModType::Flan; + } + else if(rawType == QString("dependency") || rawType == QString("depandency")) { + return ATLauncher::ModType::Dependency; + } + else if(rawType == QString("ic2lib")) { + return ATLauncher::ModType::Ic2Lib; + } + else if(rawType == QString("denlib")) { + return ATLauncher::ModType::DenLib; + } + else if(rawType == QString("coremods")) { + return ATLauncher::ModType::Coremods; + } + else if(rawType == QString("mcpc")) { + return ATLauncher::ModType::MCPC; + } + else if(rawType == QString("plugins")) { + return ATLauncher::ModType::Plugins; + } + else if(rawType == QString("extract")) { + return ATLauncher::ModType::Extract; + } + else if(rawType == QString("decomp")) { + return ATLauncher::ModType::Decomp; + } + else if(rawType == QString("texturepack")) { + return ATLauncher::ModType::TexturePack; + } + else if(rawType == QString("resourcepack")) { + return ATLauncher::ModType::ResourcePack; + } + else if(rawType == QString("shaderpack")) { + return ATLauncher::ModType::ShaderPack; + } + else if(rawType == QString("texturepackextract")) { + return ATLauncher::ModType::TexturePackExtract; + } + else if(rawType == QString("resourcepackextract")) { + return ATLauncher::ModType::ResourcePackExtract; + } + else if(rawType == QString("millenaire")) { + return ATLauncher::ModType::Millenaire; + } + + return ATLauncher::ModType::Unknown; +} + +static void loadVersionLoader(ATLauncher::VersionLoader & p, QJsonObject & obj) { + p.type = Json::requireString(obj, "type"); + p.choose = Json::ensureBoolean(obj, QString("choose"), false); + + auto metadata = Json::requireObject(obj, "metadata"); + p.latest = Json::ensureBoolean(metadata, QString("latest"), false); + p.recommended = Json::ensureBoolean(metadata, QString("recommended"), false); + + // Minecraft Forge + if (p.type == "forge") { + p.version = Json::ensureString(metadata, "version", ""); + } + + // Fabric Loader + if (p.type == "fabric") { + p.version = Json::ensureString(metadata, "loader", ""); + } +} + +static void loadVersionLibrary(ATLauncher::VersionLibrary & p, QJsonObject & obj) { + p.url = Json::requireString(obj, "url"); + p.file = Json::requireString(obj, "file"); + p.md5 = Json::requireString(obj, "md5"); + + p.download_raw = Json::requireString(obj, "download"); + p.download = parseDownloadType(p.download_raw); + + p.server = Json::ensureString(obj, "server", ""); +} + +static void loadVersionConfigs(ATLauncher::VersionConfigs & p, QJsonObject & obj) { + p.filesize = Json::requireInteger(obj, "filesize"); + p.sha1 = Json::requireString(obj, "sha1"); +} + +static void loadVersionMod(ATLauncher::VersionMod & p, QJsonObject & obj) { + p.name = Json::requireString(obj, "name"); + p.version = Json::requireString(obj, "version"); + p.url = Json::requireString(obj, "url"); + p.file = Json::requireString(obj, "file"); + p.md5 = Json::ensureString(obj, "md5", ""); + + p.download_raw = Json::requireString(obj, "download"); + p.download = parseDownloadType(p.download_raw); + + p.type_raw = Json::requireString(obj, "type"); + p.type = parseModType(p.type_raw); + + // This contributes to the Minecraft Forge detection, where we rely on mod.type being "Forge" + // when the mod represents Forge. As there is little difference between "Jar" and "Forge, some + // packs regretfully use "Jar". This will correct the type to "Forge" in these cases (as best + // it can). + if(p.name == QString("Minecraft Forge") && p.type == ATLauncher::ModType::Jar) { + p.type_raw = "forge"; + p.type = ATLauncher::ModType::Forge; + } + + if(obj.contains("extractTo")) { + p.extractTo_raw = Json::requireString(obj, "extractTo"); + p.extractTo = parseModType(p.extractTo_raw); + p.extractFolder = Json::ensureString(obj, "extractFolder", "").replace("%s%", "/"); + } + + if(obj.contains("decompType")) { + p.decompType_raw = Json::requireString(obj, "decompType"); + p.decompType = parseModType(p.decompType_raw); + p.decompFile = Json::requireString(obj, "decompFile"); + } + + p.description = Json::ensureString(obj, QString("description"), ""); + p.optional = Json::ensureBoolean(obj, QString("optional"), false); + p.recommended = Json::ensureBoolean(obj, QString("recommended"), false); + p.selected = Json::ensureBoolean(obj, QString("selected"), false); + p.hidden = Json::ensureBoolean(obj, QString("hidden"), false); + p.library = Json::ensureBoolean(obj, QString("library"), false); + p.group = Json::ensureString(obj, QString("group"), ""); + if(obj.contains("depends")) { + auto dependsArr = Json::requireArray(obj, "depends"); + for (const auto depends : dependsArr) { + p.depends.append(Json::requireValueString(depends)); + } + } + p.colour = Json::ensureString(obj, QString("colour"), ""); + p.warning = Json::ensureString(obj, QString("warning"), ""); + + p.client = Json::ensureBoolean(obj, QString("client"), false); + + // computed + p.effectively_hidden = p.hidden || p.library; +} + +static void loadVersionMainClass(ATLauncher::PackVersionMainClass & m, QJsonObject & obj) +{ + m.mainClass = Json::ensureString(obj, "mainClass", ""); + m.depends = Json::ensureString(obj, "depends", ""); +} + +static void loadVersionExtraArguments(ATLauncher::PackVersionExtraArguments & a, QJsonObject & obj) +{ + a.arguments = Json::ensureString(obj, "arguments", ""); + a.depends = Json::ensureString(obj, "depends", ""); +} + +static void loadVersionMessages(ATLauncher::VersionMessages & m, QJsonObject & obj) +{ + m.install = Json::ensureString(obj, "install", ""); + m.update = Json::ensureString(obj, "update", ""); +} + +void ATLauncher::loadVersion(PackVersion & v, QJsonObject & obj) +{ + v.version = Json::requireString(obj, "version"); + v.minecraft = Json::requireString(obj, "minecraft"); + v.noConfigs = Json::ensureBoolean(obj, QString("noConfigs"), false); + + if(obj.contains("mainClass")) { + auto main = Json::requireObject(obj, "mainClass"); + loadVersionMainClass(v.mainClass, main); + } + + if(obj.contains("extraArguments")) { + auto arguments = Json::requireObject(obj, "extraArguments"); + loadVersionExtraArguments(v.extraArguments, arguments); + } + + if(obj.contains("loader")) { + auto loader = Json::requireObject(obj, "loader"); + loadVersionLoader(v.loader, loader); + } + + if(obj.contains("libraries")) { + auto libraries = Json::requireArray(obj, "libraries"); + for (const auto libraryRaw : libraries) + { + auto libraryObj = Json::requireValueObject(libraryRaw); + ATLauncher::VersionLibrary target; + loadVersionLibrary(target, libraryObj); + v.libraries.append(target); + } + } + + if(obj.contains("mods")) { + auto mods = Json::requireArray(obj, "mods"); + for (const auto modRaw : mods) + { + auto modObj = Json::requireValueObject(modRaw); + ATLauncher::VersionMod mod; + loadVersionMod(mod, modObj); + v.mods.append(mod); + } + } + + if(obj.contains("configs")) { + auto configsObj = Json::requireObject(obj, "configs"); + loadVersionConfigs(v.configs, configsObj); + } + + if(obj.contains("colours")) { + auto colourObj = Json::requireObject(obj, "colours"); + + for (const auto &key : colourObj.keys()) { + v.colours[key] = Json::requireValueString(colourObj.value(key), "colour"); + } + } + + if(obj.contains("warnings")) { + auto warningsObj = Json::requireObject(obj, "warnings"); + + for (const auto &key : warningsObj.keys()) { + v.warnings[key] = Json::requireValueString(warningsObj.value(key), "warning"); + } + } + + if(obj.contains("messages")) { + auto messages = Json::requireObject(obj, "messages"); + loadVersionMessages(v.messages, messages); + } +} diff --git a/ultimmc/launcher/modplatform/atlauncher/ATLPackManifest.h b/ultimmc/launcher/modplatform/atlauncher/ATLPackManifest.h new file mode 100644 index 0000000..c7ad1c6 --- /dev/null +++ b/ultimmc/launcher/modplatform/atlauncher/ATLPackManifest.h @@ -0,0 +1,167 @@ +/* + * Copyright 2020-2022 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace ATLauncher +{ + +enum class PackType +{ + Public, + Private +}; + +enum class ModType +{ + Root, + Forge, + Jar, + Mods, + Flan, + Dependency, + Ic2Lib, + DenLib, + Coremods, + MCPC, + Plugins, + Extract, + Decomp, + TexturePack, + ResourcePack, + ShaderPack, + TexturePackExtract, + ResourcePackExtract, + Millenaire, + Unknown +}; + +enum class DownloadType +{ + Server, + Browser, + Direct, + Unknown +}; + +struct VersionLoader +{ + QString type; + bool latest; + bool recommended; + bool choose; + + QString version; +}; + +struct VersionLibrary +{ + QString url; + QString file; + QString server; + QString md5; + DownloadType download; + QString download_raw; +}; + +struct VersionMod +{ + QString name; + QString version; + QString url; + QString file; + QString md5; + DownloadType download; + QString download_raw; + ModType type; + QString type_raw; + + ModType extractTo; + QString extractTo_raw; + QString extractFolder; + + ModType decompType; + QString decompType_raw; + QString decompFile; + + QString description; + bool optional; + bool recommended; + bool selected; + bool hidden; + bool library; + QString group; + QVector depends; + QString colour; + QString warning; + + bool client; + + // computed + bool effectively_hidden; +}; + +struct VersionConfigs +{ + int filesize; + QString sha1; +}; + +struct PackVersionMainClass +{ + QString mainClass; + QString depends; +}; + +struct PackVersionExtraArguments +{ + QString arguments; + QString depends; +}; + +struct VersionMessages +{ + QString install; + QString update; +}; + +struct PackVersion +{ + QString version; + QString minecraft; + bool noConfigs; + PackVersionMainClass mainClass; + PackVersionExtraArguments extraArguments; + + VersionLoader loader; + QVector libraries; + QVector mods; + VersionConfigs configs; + + QMap colours; + QMap warnings; + VersionMessages messages; +}; + +void loadVersion(PackVersion & v, QJsonObject & obj); + +} diff --git a/ultimmc/launcher/modplatform/legacy_ftb/PackFetchTask.cpp b/ultimmc/launcher/modplatform/legacy_ftb/PackFetchTask.cpp new file mode 100644 index 0000000..961fe86 --- /dev/null +++ b/ultimmc/launcher/modplatform/legacy_ftb/PackFetchTask.cpp @@ -0,0 +1,172 @@ +#include "PackFetchTask.h" +#include "PrivatePackManager.h" + +#include +#include "BuildConfig.h" +#include "Application.h" + +namespace LegacyFTB { + +void PackFetchTask::fetch() +{ + publicPacks.clear(); + thirdPartyPacks.clear(); + + jobPtr = new NetJob("LegacyFTB::ModpackFetch", m_network); + + QUrl publicPacksUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/modpacks.xml"); + qDebug() << "Downloading public version info from" << publicPacksUrl.toString(); + jobPtr->addNetAction(Net::Download::makeByteArray(publicPacksUrl, &publicModpacksXmlFileData)); + + QUrl thirdPartyUrl = QUrl(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/thirdparty.xml"); + qDebug() << "Downloading thirdparty version info from" << thirdPartyUrl.toString(); + jobPtr->addNetAction(Net::Download::makeByteArray(thirdPartyUrl, &thirdPartyModpacksXmlFileData)); + + QObject::connect(jobPtr.get(), &NetJob::succeeded, this, &PackFetchTask::fileDownloadFinished); + QObject::connect(jobPtr.get(), &NetJob::failed, this, &PackFetchTask::fileDownloadFailed); + + jobPtr->start(); +} + +void PackFetchTask::fetchPrivate(const QStringList & toFetch) +{ + QString privatePackBaseUrl = BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1.xml"; + + for (auto &packCode: toFetch) + { + QByteArray *data = new QByteArray(); + NetJob *job = new NetJob("Fetching private pack", m_network); + job->addNetAction(Net::Download::makeByteArray(privatePackBaseUrl.arg(packCode), data)); + + QObject::connect(job, &NetJob::succeeded, this, [this, job, data, packCode] + { + ModpackList packs; + parseAndAddPacks(*data, PackType::Private, packs); + foreach(Modpack currentPack, packs) + { + currentPack.packCode = packCode; + emit privateFileDownloadFinished(currentPack); + } + + job->deleteLater(); + + data->clear(); + delete data; + }); + + QObject::connect(job, &NetJob::failed, this, [this, job, packCode, data](QString reason) + { + emit privateFileDownloadFailed(reason, packCode); + job->deleteLater(); + + data->clear(); + delete data; + }); + + job->start(); + } +} + +void PackFetchTask::fileDownloadFinished() +{ + jobPtr.reset(); + + QStringList failedLists; + + if(!parseAndAddPacks(publicModpacksXmlFileData, PackType::Public, publicPacks)) + { + failedLists.append(tr("Public Packs")); + } + + if(!parseAndAddPacks(thirdPartyModpacksXmlFileData, PackType::ThirdParty, thirdPartyPacks)) + { + failedLists.append(tr("Third Party Packs")); + } + + if(failedLists.size() > 0) + { + emit failed(tr("Failed to download some pack lists: %1").arg(failedLists.join("\n- "))); + } + else + { + emit finished(publicPacks, thirdPartyPacks); + } +} + +bool PackFetchTask::parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list) +{ + QDomDocument doc; + + QString errorMsg = "Unknown error."; + int errorLine = -1; + int errorCol = -1; + + if(!doc.setContent(data, false, &errorMsg, &errorLine, &errorCol)) + { + auto fullErrMsg = QString("Failed to fetch modpack data: %1 %2:3d!").arg(errorMsg, errorLine, errorCol); + qWarning() << fullErrMsg; + data.clear(); + return false; + } + + QDomNodeList nodes = doc.elementsByTagName("modpack"); + for(int i = 0; i < nodes.length(); i++) + { + QDomElement element = nodes.at(i).toElement(); + + Modpack modpack; + modpack.name = element.attribute("name"); + modpack.currentVersion = element.attribute("version"); + modpack.mcVersion = element.attribute("mcVersion"); + modpack.description = element.attribute("description"); + modpack.mods = element.attribute("mods"); + modpack.logo = element.attribute("logo"); + modpack.oldVersions = element.attribute("oldVersions").split(";"); + modpack.broken = false; + modpack.bugged = false; + + //remove empty if the xml is bugged + for(QString curr : modpack.oldVersions) + { + if(curr.isNull() || curr.isEmpty()) + { + modpack.oldVersions.removeAll(curr); + modpack.bugged = true; + qWarning() << "Removed some empty versions from" << modpack.name; + } + } + + if(modpack.oldVersions.size() < 1) + { + if(!modpack.currentVersion.isNull() && !modpack.currentVersion.isEmpty()) + { + modpack.oldVersions.append(modpack.currentVersion); + qWarning() << "Added current version to oldVersions because oldVersions was empty! (" + modpack.name + ")"; + } + else + { + modpack.broken = true; + qWarning() << "Broken pack:" << modpack.name << " => No valid version!"; + } + } + + modpack.author = element.attribute("author"); + + modpack.dir = element.attribute("dir"); + modpack.file = element.attribute("url"); + + modpack.type = packType; + + list.append(modpack); + } + + return true; +} + +void PackFetchTask::fileDownloadFailed(QString reason) +{ + qWarning() << "Fetching FTBPacks failed:" << reason; + emit failed(reason); +} + +} diff --git a/ultimmc/launcher/modplatform/legacy_ftb/PackFetchTask.h b/ultimmc/launcher/modplatform/legacy_ftb/PackFetchTask.h new file mode 100644 index 0000000..f1667e9 --- /dev/null +++ b/ultimmc/launcher/modplatform/legacy_ftb/PackFetchTask.h @@ -0,0 +1,45 @@ +#pragma once + +#include "net/NetJob.h" +#include +#include +#include +#include "PackHelpers.h" + +namespace LegacyFTB { + +class PackFetchTask : public QObject { + + Q_OBJECT + +public: + PackFetchTask(shared_qobject_ptr network) : QObject(nullptr), m_network(network) {}; + virtual ~PackFetchTask() = default; + + void fetch(); + void fetchPrivate(const QStringList &toFetch); + +private: + shared_qobject_ptr m_network; + NetJob::Ptr jobPtr; + + QByteArray publicModpacksXmlFileData; + QByteArray thirdPartyModpacksXmlFileData; + + bool parseAndAddPacks(QByteArray &data, PackType packType, ModpackList &list); + ModpackList publicPacks; + ModpackList thirdPartyPacks; + +protected slots: + void fileDownloadFinished(); + void fileDownloadFailed(QString reason); + +signals: + void finished(ModpackList publicPacks, ModpackList thirdPartyPacks); + void failed(QString reason); + + void privateFileDownloadFinished(Modpack modpack); + void privateFileDownloadFailed(QString reason, QString packCode); +}; + +} diff --git a/ultimmc/launcher/modplatform/legacy_ftb/PackHelpers.h b/ultimmc/launcher/modplatform/legacy_ftb/PackHelpers.h new file mode 100644 index 0000000..566210d --- /dev/null +++ b/ultimmc/launcher/modplatform/legacy_ftb/PackHelpers.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include + +namespace LegacyFTB { + +//Header for structs etc... +enum class PackType +{ + Public, + ThirdParty, + Private +}; + +struct Modpack +{ + QString name; + QString description; + QString author; + QStringList oldVersions; + QString currentVersion; + QString mcVersion; + QString mods; + QString logo; + + //Technical data + QString dir; + QString file; //<- Url in the xml, but doesn't make much sense + + bool bugged = false; + bool broken = false; + + PackType type; + QString packCode; +}; + +typedef QList ModpackList; + +} + +//We need it for the proxy model +Q_DECLARE_METATYPE(LegacyFTB::Modpack) diff --git a/ultimmc/launcher/modplatform/legacy_ftb/PackInstallTask.cpp b/ultimmc/launcher/modplatform/legacy_ftb/PackInstallTask.cpp new file mode 100644 index 0000000..1d30019 --- /dev/null +++ b/ultimmc/launcher/modplatform/legacy_ftb/PackInstallTask.cpp @@ -0,0 +1,214 @@ +#include "PackInstallTask.h" + +#include + +#include "MMCZip.h" +#include "BaseInstance.h" +#include "FileSystem.h" +#include "settings/INISettingsObject.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "minecraft/GradleSpecifier.h" + +#include "BuildConfig.h" +#include "Application.h" + +namespace LegacyFTB { + +PackInstallTask::PackInstallTask(shared_qobject_ptr network, Modpack pack, QString version) +{ + m_pack = pack; + m_version = version; + m_network = network; +} + +void PackInstallTask::executeTask() +{ + downloadPack(); +} + +void PackInstallTask::downloadPack() +{ + setStatus(tr("Downloading zip for %1").arg(m_pack.name)); + + auto packoffset = QString("%1/%2/%3").arg(m_pack.dir, m_version.replace(".", "_"), m_pack.file); + auto entry = APPLICATION->metacache()->resolveEntry("FTBPacks", packoffset); + netJobContainer = new NetJob("Download FTB Pack", m_network); + + entry->setStale(true); + QString url; + if(m_pack.type == PackType::Private) + { + url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "privatepacks/%1").arg(packoffset); + } + else + { + url = QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "modpacks/%1").arg(packoffset); + } + netJobContainer->addNetAction(Net::Download::makeCached(url, entry)); + archivePath = entry->getFullPath(); + + connect(netJobContainer.get(), &NetJob::succeeded, this, &PackInstallTask::onDownloadSucceeded); + connect(netJobContainer.get(), &NetJob::failed, this, &PackInstallTask::onDownloadFailed); + connect(netJobContainer.get(), &NetJob::progress, this, &PackInstallTask::onDownloadProgress); + netJobContainer->start(); + + progress(1, 4); +} + +void PackInstallTask::onDownloadSucceeded() +{ + abortable = false; + unzip(); +} + +void PackInstallTask::onDownloadFailed(QString reason) +{ + abortable = false; + emitFailed(reason); +} + +void PackInstallTask::onDownloadProgress(qint64 current, qint64 total) +{ + abortable = true; + progress(current, total * 4); + setStatus(tr("Downloading zip for %1 (%2%)").arg(m_pack.name).arg(current / 10)); +} + +void PackInstallTask::unzip() +{ + progress(2, 4); + setStatus(tr("Extracting modpack")); + QDir extractDir(m_stagingPath); + + m_packZip.reset(new QuaZip(archivePath)); + if(!m_packZip->open(QuaZip::mdUnzip)) + { + emitFailed(tr("Failed to open modpack file %1!").arg(archivePath)); + return; + } + + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractDir, archivePath, extractDir.absolutePath() + "/unzip"); + connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::onUnzipFinished); + connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::onUnzipCanceled); + m_extractFutureWatcher.setFuture(m_extractFuture); +} + +void PackInstallTask::onUnzipFinished() +{ + install(); +} + +void PackInstallTask::onUnzipCanceled() +{ + emitAborted(); +} + +void PackInstallTask::install() +{ + progress(3, 4); + setStatus(tr("Installing modpack")); + QDir unzipMcDir(m_stagingPath + "/unzip/minecraft"); + if(unzipMcDir.exists()) + { + //ok, found minecraft dir, move contents to instance dir + if(!QDir().rename(m_stagingPath + "/unzip/minecraft", m_stagingPath + "/.minecraft")) + { + emitFailed(tr("Failed to move unzipped minecraft!")); + return; + } + } + + QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(instanceConfigPath); + instanceSettings->suspendSave(); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); + + bool fallback = true; + + //handle different versions + QFile packJson(m_stagingPath + "/.minecraft/pack.json"); + QDir jarmodDir = QDir(m_stagingPath + "/unzip/instMods"); + if(packJson.exists()) + { + packJson.open(QIODevice::ReadOnly | QIODevice::Text); + QJsonDocument doc = QJsonDocument::fromJson(packJson.readAll()); + packJson.close(); + + //we only care about the libs + QJsonArray libs = doc.object().value("libraries").toArray(); + + foreach (const QJsonValue &value, libs) + { + QString nameValue = value.toObject().value("name").toString(); + if(!nameValue.startsWith("net.minecraftforge")) + { + continue; + } + + GradleSpecifier forgeVersion(nameValue); + + components->setComponentVersion("net.minecraftforge", forgeVersion.version().replace(m_pack.mcVersion, "").replace("-", "")); + packJson.remove(); + fallback = false; + break; + } + + } + + if(jarmodDir.exists()) + { + qDebug() << "Found jarmods, installing..."; + + QStringList jarmods; + for (auto info: jarmodDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files)) + { + qDebug() << "Jarmod:" << info.fileName(); + jarmods.push_back(info.absoluteFilePath()); + } + + components->installJarMods(jarmods); + fallback = false; + } + + //just nuke unzip directory, it s not needed anymore + FS::deletePath(m_stagingPath + "/unzip"); + + if(fallback) + { + //TODO: Some fallback mechanism... or just keep failing! + emitFailed(tr("No installation method found!")); + return; + } + + components->saveNow(); + + progress(4, 4); + + instance.setName(m_instName); + if(m_instIcon == "default") + { + m_instIcon = "ftb_logo"; + } + instance.setIconKey(m_instIcon); + instanceSettings->resumeSave(); + + emitSucceeded(); +} + +bool PackInstallTask::abort() +{ + if(abortable) + { + return netJobContainer->abort(); + } + return false; +} + +} diff --git a/ultimmc/launcher/modplatform/legacy_ftb/PackInstallTask.h b/ultimmc/launcher/modplatform/legacy_ftb/PackInstallTask.h new file mode 100644 index 0000000..305635a --- /dev/null +++ b/ultimmc/launcher/modplatform/legacy_ftb/PackInstallTask.h @@ -0,0 +1,58 @@ +#pragma once +#include "InstanceTask.h" +#include "net/NetJob.h" +#include "quazip.h" +#include "quazipdir.h" +#include "meta/Index.h" +#include "meta/Version.h" +#include "meta/VersionList.h" +#include "PackHelpers.h" + +#include "net/NetJob.h" + +#include + +namespace LegacyFTB { + +class PackInstallTask : public InstanceTask +{ + Q_OBJECT + +public: + explicit PackInstallTask(shared_qobject_ptr network, Modpack pack, QString version); + virtual ~PackInstallTask(){} + + bool canAbort() const override { return true; } + bool abort() override; + +protected: + //! Entry point for tasks. + virtual void executeTask() override; + +private: + void downloadPack(); + void unzip(); + void install(); + +private slots: + void onDownloadSucceeded(); + void onDownloadFailed(QString reason); + void onDownloadProgress(qint64 current, qint64 total); + + void onUnzipFinished(); + void onUnzipCanceled(); + +private: /* data */ + shared_qobject_ptr m_network; + bool abortable = false; + std::unique_ptr m_packZip; + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; + NetJob::Ptr netJobContainer; + QString archivePath; + + Modpack m_pack; + QString m_version; +}; + +} diff --git a/ultimmc/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp b/ultimmc/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp new file mode 100644 index 0000000..501e600 --- /dev/null +++ b/ultimmc/launcher/modplatform/legacy_ftb/PrivatePackManager.cpp @@ -0,0 +1,41 @@ +#include "PrivatePackManager.h" + +#include + +#include "FileSystem.h" + +namespace LegacyFTB { + +void PrivatePackManager::load() +{ + try + { + currentPacks = QString::fromUtf8(FS::read(m_filename)).split('\n', QString::SkipEmptyParts).toSet(); + dirty = false; + } + catch(...) + { + currentPacks = {}; + qWarning() << "Failed to read third party FTB pack codes from" << m_filename; + } +} + +void PrivatePackManager::save() const +{ + if(!dirty) + { + return; + } + try + { + QStringList list = currentPacks.toList(); + FS::write(m_filename, list.join('\n').toUtf8()); + dirty = false; + } + catch(...) + { + qWarning() << "Failed to write third party FTB pack codes to" << m_filename; + } +} + +} diff --git a/ultimmc/launcher/modplatform/legacy_ftb/PrivatePackManager.h b/ultimmc/launcher/modplatform/legacy_ftb/PrivatePackManager.h new file mode 100644 index 0000000..0e81464 --- /dev/null +++ b/ultimmc/launcher/modplatform/legacy_ftb/PrivatePackManager.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +namespace LegacyFTB { + +class PrivatePackManager +{ +public: + ~PrivatePackManager() + { + save(); + } + void load(); + void save() const; + bool empty() const + { + return currentPacks.empty(); + } + const QSet &getCurrentPackCodes() const + { + return currentPacks; + } + void add(const QString &code) + { + currentPacks.insert(code); + dirty = true; + } + void remove(const QString &code) + { + currentPacks.remove(code); + dirty = true; + } + +private: + QSet currentPacks; + QString m_filename = "private_packs.txt"; + mutable bool dirty = false; +}; + +} diff --git a/ultimmc/launcher/modplatform/modrinth/ModrinthHashLookupRequest.cpp b/ultimmc/launcher/modplatform/modrinth/ModrinthHashLookupRequest.cpp new file mode 100644 index 0000000..4edb912 --- /dev/null +++ b/ultimmc/launcher/modplatform/modrinth/ModrinthHashLookupRequest.cpp @@ -0,0 +1,124 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include +#include +#include "ModrinthHashLookupRequest.h" +#include "BuildConfig.h" +#include "Json.h" + +namespace Modrinth +{ + +HashLookupRequest::HashLookupRequest(QList hashes, QList *output) : NetAction(), m_hashes(hashes), m_output(output) +{ + m_url = "https://api.modrinth.com/v2/version_files"; + m_status = Job_NotStarted; +} + +void HashLookupRequest::startImpl() +{ + finished = false; + m_status = Job_InProgress; + + QNetworkRequest request(m_url); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + QJsonObject requestObject; + QJsonArray hashes; + + for (const auto &data : m_hashes) { + hashes.append(data.hash); + } + + requestObject.insert("hashes", hashes); + requestObject.insert("algorithm", QJsonValue("sha512")); + + QNetworkReply *rep = m_network->post(request, QJsonDocument(requestObject).toJson()); + m_reply.reset(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &HashLookupRequest::downloadProgress); + connect(rep, &QNetworkReply::finished, this, &HashLookupRequest::downloadFinished); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); +} + +void HashLookupRequest::downloadError(QNetworkReply::NetworkError error) +{ + qCritical() << "Modrinth hash lookup request failed with error" << m_reply->errorString() << "Server reply:\n" << m_reply->readAll(); + if (finished) { + qCritical() << "Double finished ModrinthHashLookupRequest!"; + return; + } + m_status = Job_Failed; + finished = true; + m_reply.reset(); + emit failed(m_index_within_job); +} + +void HashLookupRequest::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + m_total_progress = bytesTotal; + m_progress = bytesReceived; + emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); +} + +void HashLookupRequest::downloadFinished() +{ + if (finished) { + qCritical() << "Double finished ModrinthHashLookupRequest!"; + return; + } + + QByteArray data = m_reply->readAll(); + m_reply.reset(); + + try { + auto document = Json::requireDocument(data); + auto rootObject = Json::requireObject(document); + + for (const auto &hashData : m_hashes) { + if (rootObject.contains(hashData.hash)) { + auto versionObject = Json::requireObject(rootObject, hashData.hash); + + auto files = Json::requireIsArrayOf(versionObject, "files"); + + QJsonObject file; + + for (const auto &fileJson : files) { + auto hashes = Json::requireObject(fileJson, "hashes"); + QString sha512 = Json::requireString(hashes, "sha512"); + + if (sha512 == hashData.hash) { + file = fileJson; + } + } + + m_output->append(HashLookupResponseData { + hashData.fileInfo, + true, + file + }); + } else { + m_output->append(HashLookupResponseData { + hashData.fileInfo, + false, + QJsonObject() + }); + } + } + + m_status = Job_Finished; + finished = true; + emit succeeded(m_index_within_job); + } catch (const Json::JsonException &e) { + qCritical() << "Failed to parse Modrinth hash lookup response: " << e.cause(); + m_status = Job_Failed; + finished = true; + emit failed(m_index_within_job); + } +} +} \ No newline at end of file diff --git a/ultimmc/launcher/modplatform/modrinth/ModrinthHashLookupRequest.h b/ultimmc/launcher/modplatform/modrinth/ModrinthHashLookupRequest.h new file mode 100644 index 0000000..a3a803f --- /dev/null +++ b/ultimmc/launcher/modplatform/modrinth/ModrinthHashLookupRequest.h @@ -0,0 +1,55 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include +#include +#include "net/NetAction.h" + +namespace Modrinth +{ + +struct HashLookupData +{ + QFileInfo fileInfo; + QString hash; +}; + +struct HashLookupResponseData +{ + QFileInfo fileInfo; + bool found; + QJsonObject fileJson; +}; + +class HashLookupRequest : public NetAction +{ +public: + using Ptr = shared_qobject_ptr; + + explicit HashLookupRequest(QList hashes, QList *output); + static Ptr make(QList hashes, QList *output) { + return Ptr(new HashLookupRequest(hashes, output)); + } + +protected slots: + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; + void downloadError(QNetworkReply::NetworkError error) override; + void downloadFinished() override; + void downloadReadyRead() override {} + +public slots: + void startImpl() override; + +private: + QList m_hashes; + std::shared_ptr> m_output; + bool finished = true; +}; + +} \ No newline at end of file diff --git a/ultimmc/launcher/modplatform/modrinth/ModrinthInstanceExportTask.cpp b/ultimmc/launcher/modplatform/modrinth/ModrinthInstanceExportTask.cpp new file mode 100644 index 0000000..be57fbb --- /dev/null +++ b/ultimmc/launcher/modplatform/modrinth/ModrinthInstanceExportTask.cpp @@ -0,0 +1,277 @@ +/* + * Copyright 2023-2024 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include +#include +#include +#include +#include "Json.h" +#include "ModrinthInstanceExportTask.h" +#include "net/NetJob.h" +#include "Application.h" +#include "ui/dialogs/ModrinthExportDialog.h" +#include "JlCompress.h" +#include "FileSystem.h" +#include "ModrinthHashLookupRequest.h" + +namespace Modrinth +{ + +InstanceExportTask::InstanceExportTask(InstancePtr instance, ExportSettings settings) : m_instance(instance), m_settings(settings) {} + +void InstanceExportTask::executeTask() +{ + setStatus(tr("Finding files to look up on Modrinth...")); + + QDir modsDir(m_instance->gameRoot() + "/mods"); + modsDir.setFilter(QDir::Files); + QStringList modsFilter = { "*.jar" }; + if (m_settings.treatDisabledAsOptional) { + modsFilter << "*.jar.disabled"; + } + modsDir.setNameFilters(modsFilter); + + QStringList zipFilter = { "*.zip" }; + if (m_settings.treatDisabledAsOptional) { + zipFilter << "*.zip.disabled"; + } + + QDir resourcePacksDir(m_instance->gameRoot() + "/resourcepacks"); + resourcePacksDir.setFilter(QDir::Files); + resourcePacksDir.setNameFilters(zipFilter); + + QDir shaderPacksDir(m_instance->gameRoot() + "/shaderpacks"); + shaderPacksDir.setFilter(QDir::Files); + shaderPacksDir.setNameFilters(zipFilter); + + QStringList filesToResolve; + + if (modsDir.exists()) { + QDirIterator modsIterator(modsDir); + while (modsIterator.hasNext()) { + filesToResolve << modsIterator.next(); + } + } + + if (m_settings.includeResourcePacks && resourcePacksDir.exists()) { + QDirIterator resourcePacksIterator(resourcePacksDir); + while (resourcePacksIterator.hasNext()) { + filesToResolve << resourcePacksIterator.next(); + } + } + + if (m_settings.includeShaderPacks && shaderPacksDir.exists()) { + QDirIterator shaderPacksIterator(shaderPacksDir); + while (shaderPacksIterator.hasNext()) { + filesToResolve << shaderPacksIterator.next(); + } + } + + if (!m_settings.datapacksPath.isEmpty()) { + QDir datapacksDir(m_instance->gameRoot() + "/" + m_settings.datapacksPath); + datapacksDir.setFilter(QDir::Files); + datapacksDir.setNameFilters(QStringList() << "*.zip"); + + if (datapacksDir.exists()) { + QDirIterator datapacksIterator(datapacksDir); + while (datapacksIterator.hasNext()) { + filesToResolve << datapacksIterator.next(); + } + } + } + + m_netJob = new NetJob(tr("Modrinth pack export"), APPLICATION->network()); + + QList hashes; + + qint64 progress = 0; + setProgress(progress, filesToResolve.length()); + for (const QString &filePath: filesToResolve) { + qDebug() << "Attempting to resolve file hash from Modrinth API: " << filePath; + QFile file(filePath); + + if (file.open(QFile::ReadOnly)) { + QByteArray contents = file.readAll(); + QCryptographicHash hasher(QCryptographicHash::Sha512); + hasher.addData(contents); + QString hash = hasher.result().toHex(); + + hashes.append(HashLookupData { + QFileInfo(file), + hash + }); + + progress++; + setProgress(progress, filesToResolve.length()); + } + } + + m_response.reset(new QList); + + m_netJob->addNetAction(HashLookupRequest::make(hashes, m_response.get())); + + connect(m_netJob.get(), &NetJob::succeeded, this, &InstanceExportTask::lookupSucceeded); + connect(m_netJob.get(), &NetJob::failed, this, &InstanceExportTask::lookupFailed); + connect(m_netJob.get(), &NetJob::progress, this, &InstanceExportTask::lookupProgress); + + m_netJob->start(); + setStatus(tr("Looking up files on Modrinth...")); +} + +void InstanceExportTask::lookupSucceeded() +{ + setStatus(tr("Creating modpack metadata...")); + QList resolvedFiles; + QFileInfoList failedFiles; + + for (const auto &file : *m_response) { + if (file.found) { + try { + auto url = Json::requireString(file.fileJson, "url"); + auto hashes = Json::requireObject(file.fileJson, "hashes"); + + QString sha512Hash = Json::requireString(hashes, "sha512"); + QString sha1Hash = Json::requireString(hashes, "sha1"); + + ExportFile fileData; + + QDir gameDir(m_instance->gameRoot()); + + QString path = file.fileInfo.absoluteFilePath(); + if (path.endsWith(".disabled")) { + fileData.optional = true; + path = path.left(path.length() - QString(".disabled").length()); + } + + fileData.path = gameDir.relativeFilePath(path); + fileData.download = url; + fileData.sha512 = sha512Hash; + fileData.sha1 = sha1Hash; + fileData.fileSize = file.fileInfo.size(); + + resolvedFiles << fileData; + } catch (const Json::JsonException &e) { + qDebug() << "File " << file.fileInfo.absoluteFilePath() << " failed to process for reason " << e.cause() << ", adding to overrides"; + failedFiles << file.fileInfo; + } + } else { + failedFiles << file.fileInfo; + } + } + + QJsonObject indexJson; + indexJson.insert("formatVersion", QJsonValue(1)); + indexJson.insert("game", QJsonValue("minecraft")); + indexJson.insert("versionId", QJsonValue(m_settings.version)); + indexJson.insert("name", QJsonValue(m_settings.name)); + + if (!m_settings.description.isEmpty()) { + indexJson.insert("summary", QJsonValue(m_settings.description)); + } + + QJsonArray files; + + for (const auto &file : resolvedFiles) { + QJsonObject fileObj; + fileObj.insert("path", file.path); + + QJsonObject hashes; + hashes.insert("sha512", file.sha512); + hashes.insert("sha1", file.sha1); + fileObj.insert("hashes", hashes); + + QJsonArray downloads; + downloads.append(file.download); + fileObj.insert("downloads", downloads); + + fileObj.insert("fileSize", QJsonValue(file.fileSize)); + + if (file.optional) { + QJsonObject env; + env.insert("client", "optional"); + env.insert("server", "optional"); + fileObj.insert("env", env); + } + + files.append(fileObj); + } + + indexJson.insert("files", files); + + QJsonObject dependencies; + dependencies.insert("minecraft", m_settings.gameVersion); + if (!m_settings.forgeVersion.isEmpty()) { + dependencies.insert("forge", m_settings.forgeVersion); + } + if (!m_settings.fabricVersion.isEmpty()) { + dependencies.insert("fabric-loader", m_settings.fabricVersion); + } + if (!m_settings.quiltVersion.isEmpty()) { + dependencies.insert("quilt-loader", m_settings.quiltVersion); + } + if (!m_settings.neoforgeVersion.isEmpty()) { + dependencies.insert("neoforge", m_settings.neoforgeVersion); + } + + indexJson.insert("dependencies", dependencies); + + setStatus(tr("Copying files to modpack...")); + + QTemporaryDir tmp; + if (tmp.isValid()) { + Json::write(indexJson, tmp.path() + "/modrinth.index.json"); + + if (!failedFiles.isEmpty()) { + QDir tmpDir(tmp.path()); + QDir gameDir(m_instance->gameRoot()); + for (const auto &file : failedFiles) { + QString src = file.absoluteFilePath(); + tmpDir.mkpath("overrides/" + gameDir.relativeFilePath(file.absolutePath())); + QString dest = tmpDir.path() + "/overrides/" + gameDir.relativeFilePath(src); + if (!QFile::copy(file.absoluteFilePath(), dest)) { + emitFailed(tr("Failed to copy file %1 to overrides").arg(src)); + return; + } + } + + if (m_settings.includeGameConfig) { + tmpDir.mkdir("overrides"); + QFile::copy(gameDir.absoluteFilePath("options.txt"), tmpDir.absoluteFilePath("overrides/options.txt")); + } + + if (m_settings.includeModConfigs) { + tmpDir.mkdir("overrides"); + FS::copy copy(m_instance->gameRoot() + "/config", tmpDir.absoluteFilePath("overrides/config")); + copy(); + } + } + + setStatus(tr("Zipping modpack...")); + if (!JlCompress::compressDir(m_settings.exportPath, tmp.path())) { + emitFailed(tr("Failed to create zip file")); + return; + } + } else { + emitFailed(tr("Failed to create temporary directory")); + return; + } + + qDebug() << "Successfully exported Modrinth pack to " << m_settings.exportPath; + emitSucceeded(); +} + +void InstanceExportTask::lookupFailed(const QString &reason) +{ + emitFailed(reason); +} + +void InstanceExportTask::lookupProgress(qint64 current, qint64 total) +{ + setProgress(current, total); +} + +} \ No newline at end of file diff --git a/ultimmc/launcher/modplatform/modrinth/ModrinthInstanceExportTask.h b/ultimmc/launcher/modplatform/modrinth/ModrinthInstanceExportTask.h new file mode 100644 index 0000000..93d4625 --- /dev/null +++ b/ultimmc/launcher/modplatform/modrinth/ModrinthInstanceExportTask.h @@ -0,0 +1,75 @@ +/* + * Copyright 2023-2024 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include "tasks/Task.h" +#include "BaseInstance.h" +#include "net/NetJob.h" +#include "ui/dialogs/ModrinthExportDialog.h" +#include "ModrinthHashLookupRequest.h" + +namespace Modrinth +{ + +struct ExportSettings +{ + QString version; + QString name; + QString description; + + bool includeGameConfig; + bool includeModConfigs; + bool includeResourcePacks; + bool includeShaderPacks; + bool treatDisabledAsOptional; + QString datapacksPath; + + QString gameVersion; + QString forgeVersion; + QString fabricVersion; + QString quiltVersion; + QString neoforgeVersion; + + QString exportPath; +}; + +// Using the existing Modrinth::File struct from the importer doesn't actually make much sense here (doesn't support multiple hashes, hash is a byte array rather than a string, no file size, etc) +struct ExportFile +{ + QString path; + QString sha512; + QString sha1; + QString download; + qint64 fileSize; + bool optional = false; +}; + +class InstanceExportTask : public Task +{ +Q_OBJECT + +public: + explicit InstanceExportTask(InstancePtr instance, ExportSettings settings); + +protected: + //! Entry point for tasks. + virtual void executeTask() override; + +private slots: + void lookupSucceeded(); + void lookupFailed(const QString &reason); + void lookupProgress(qint64 current, qint64 total); + +private: + InstancePtr m_instance; + ExportSettings m_settings; + std::shared_ptr> m_response; + NetJob::Ptr m_netJob; +}; + +} \ No newline at end of file diff --git a/ultimmc/launcher/modplatform/modrinth/ModrinthPackManifest.cpp b/ultimmc/launcher/modplatform/modrinth/ModrinthPackManifest.cpp new file mode 100644 index 0000000..2100aaf --- /dev/null +++ b/ultimmc/launcher/modplatform/modrinth/ModrinthPackManifest.cpp @@ -0,0 +1,16 @@ +/* Copyright 2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModrinthPackManifest.h" diff --git a/ultimmc/launcher/modplatform/modrinth/ModrinthPackManifest.h b/ultimmc/launcher/modplatform/modrinth/ModrinthPackManifest.h new file mode 100644 index 0000000..9742aeb --- /dev/null +++ b/ultimmc/launcher/modplatform/modrinth/ModrinthPackManifest.h @@ -0,0 +1,32 @@ +/* Copyright 2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +namespace Modrinth { +struct File +{ + QString path; + QCryptographicHash::Algorithm hashAlgorithm; + QByteArray hash; + // TODO: should this support multiple download URLs, like the JSON does? + QUrl download; +}; +} diff --git a/ultimmc/launcher/modplatform/technic/SingleZipPackInstallTask.cpp b/ultimmc/launcher/modplatform/technic/SingleZipPackInstallTask.cpp new file mode 100644 index 0000000..9093b24 --- /dev/null +++ b/ultimmc/launcher/modplatform/technic/SingleZipPackInstallTask.cpp @@ -0,0 +1,142 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SingleZipPackInstallTask.h" + +#include + +#include "MMCZip.h" +#include "TechnicPackProcessor.h" +#include "FileSystem.h" + +#include "Application.h" + +Technic::SingleZipPackInstallTask::SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion) +{ + m_sourceUrl = sourceUrl; + m_minecraftVersion = minecraftVersion; +} + +bool Technic::SingleZipPackInstallTask::abort() { + if(m_abortable) + { + return m_filesNetJob->abort(); + } + return false; +} + +void Technic::SingleZipPackInstallTask::executeTask() +{ + setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString())); + + const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path(); + auto entry = APPLICATION->metacache()->resolveEntry("general", path); + entry->setStale(true); + m_filesNetJob = new NetJob(tr("Modpack download"), APPLICATION->network()); + m_filesNetJob->addNetAction(Net::Download::makeCached(m_sourceUrl, entry)); + m_archivePath = entry->getFullPath(); + auto job = m_filesNetJob.get(); + connect(job, &NetJob::succeeded, this, &Technic::SingleZipPackInstallTask::downloadSucceeded); + connect(job, &NetJob::progress, this, &Technic::SingleZipPackInstallTask::downloadProgressChanged); + connect(job, &NetJob::failed, this, &Technic::SingleZipPackInstallTask::downloadFailed); + m_filesNetJob->start(); +} + +void Technic::SingleZipPackInstallTask::downloadSucceeded() +{ + m_abortable = false; + + setStatus(tr("Extracting modpack")); + QDir extractDir(FS::PathCombine(m_stagingPath, ".minecraft")); + qDebug() << "Attempting to create instance from" << m_archivePath; + + // open the zip and find relevant files in it + m_packZip.reset(new QuaZip(m_archivePath)); + if (!m_packZip->open(QuaZip::mdUnzip)) + { + emitFailed(tr("Unable to open supplied modpack zip file.")); + return; + } + m_extractFuture = QtConcurrent::run(QThreadPool::globalInstance(), MMCZip::extractSubDir, m_packZip.get(), QString(""), extractDir.absolutePath()); + connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &Technic::SingleZipPackInstallTask::extractFinished); + connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &Technic::SingleZipPackInstallTask::extractAborted); + m_extractFutureWatcher.setFuture(m_extractFuture); + m_filesNetJob.reset(); +} + +void Technic::SingleZipPackInstallTask::downloadFailed(QString reason) +{ + m_abortable = false; + emitFailed(reason); + m_filesNetJob.reset(); +} + +void Technic::SingleZipPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) +{ + m_abortable = true; + setProgress(current / 2, total); +} + +void Technic::SingleZipPackInstallTask::extractFinished() +{ + m_packZip.reset(); + if (!m_extractFuture.result()) + { + emitFailed(tr("Failed to extract modpack")); + return; + } + QDir extractDir(m_stagingPath); + + qDebug() << "Fixing permissions for extracted pack files..."; + QDirIterator it(extractDir, QDirIterator::Subdirectories); + while (it.hasNext()) + { + auto filepath = it.next(); + QFileInfo file(filepath); + auto permissions = QFile::permissions(filepath); + auto origPermissions = permissions; + if (file.isDir()) + { + // Folder +rwx for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser; + } + else + { + // File +rw for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + } + if (origPermissions != permissions) + { + if (!QFile::setPermissions(filepath, permissions)) + { + logWarning(tr("Could not fix permissions for %1").arg(filepath)); + } + else + { + qDebug() << "Fixed" << filepath; + } + } + } + + shared_qobject_ptr packProcessor = new Technic::TechnicPackProcessor(); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SingleZipPackInstallTask::emitSucceeded); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SingleZipPackInstallTask::emitFailed); + packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion); +} + +void Technic::SingleZipPackInstallTask::extractAborted() +{ + emitFailed(tr("Instance import has been aborted.")); +} diff --git a/ultimmc/launcher/modplatform/technic/SingleZipPackInstallTask.h b/ultimmc/launcher/modplatform/technic/SingleZipPackInstallTask.h new file mode 100644 index 0000000..74f6094 --- /dev/null +++ b/ultimmc/launcher/modplatform/technic/SingleZipPackInstallTask.h @@ -0,0 +1,64 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "InstanceTask.h" +#include "net/NetJob.h" + +#include "quazip.h" + +#include +#include +#include + +#include + +namespace Technic { + +class SingleZipPackInstallTask : public InstanceTask +{ + Q_OBJECT + +public: + SingleZipPackInstallTask(const QUrl &sourceUrl, const QString &minecraftVersion); + + bool canAbort() const override { return true; } + bool abort() override; + +protected: + void executeTask() override; + + +private slots: + void downloadSucceeded(); + void downloadFailed(QString reason); + void downloadProgressChanged(qint64 current, qint64 total); + void extractFinished(); + void extractAborted(); + +private: + bool m_abortable = false; + + QUrl m_sourceUrl; + QString m_minecraftVersion; + QString m_archivePath; + NetJob::Ptr m_filesNetJob; + std::unique_ptr m_packZip; + QFuture> m_extractFuture; + QFutureWatcher> m_extractFutureWatcher; +}; + +} // namespace Technic diff --git a/ultimmc/launcher/modplatform/technic/SolderPackInstallTask.cpp b/ultimmc/launcher/modplatform/technic/SolderPackInstallTask.cpp new file mode 100644 index 0000000..ccd0b92 --- /dev/null +++ b/ultimmc/launcher/modplatform/technic/SolderPackInstallTask.cpp @@ -0,0 +1,201 @@ +/* Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-2022 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SolderPackInstallTask.h" + +#include +#include +#include +#include +#include "TechnicPackProcessor.h" +#include "SolderPackManifest.h" +#include "net/ChecksumValidator.h" + +Technic::SolderPackInstallTask::SolderPackInstallTask( + shared_qobject_ptr network, + const QUrl &sourceUrl, + const QString &version, + const QString &minecraftVersion +) { + m_sourceUrl = sourceUrl; + m_minecraftVersion = minecraftVersion; + m_version = version; + m_network = network; +} + +bool Technic::SolderPackInstallTask::abort() { + if(m_abortable) + { + return m_filesNetJob->abort(); + } + return false; +} + +void Technic::SolderPackInstallTask::executeTask() +{ + setStatus(tr("Resolving modpack files")); + + m_filesNetJob = new NetJob(tr("Resolving modpack files"), m_network); + auto sourceUrl = QString("%1/%2").arg(m_sourceUrl.toString(), m_version); + m_filesNetJob->addNetAction(Net::Download::makeByteArray(sourceUrl, &m_response)); + + auto job = m_filesNetJob.get(); + connect(job, &NetJob::succeeded, this, &Technic::SolderPackInstallTask::fileListSucceeded); + connect(job, &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); + m_filesNetJob->start(); +} + +void Technic::SolderPackInstallTask::fileListSucceeded() +{ + setStatus(tr("Downloading modpack")); + + QJsonParseError parse_error {}; + QJsonDocument doc = QJsonDocument::fromJson(m_response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << m_response; + return; + } + auto obj = doc.object(); + + TechnicSolder::PackBuild build; + try { + TechnicSolder::loadPackBuild(build, obj); + } + catch (const JSONValidationError& e) { + emitFailed(tr("Could not understand pack manifest:\n") + e.cause()); + m_filesNetJob.reset(); + return; + } + + if (!build.minecraft.isEmpty()) + m_minecraftVersion = build.minecraft; + + m_filesNetJob = new NetJob(tr("Downloading modpack"), m_network); + int i = 0; + for (const auto &mod : build.mods) + { + auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); + + auto dl = Net::Download::makeFile(mod.url, path); + if (!mod.md5.isEmpty()) { + auto rawMd5 = QByteArray::fromHex(mod.md5.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + } + m_filesNetJob->addNetAction(dl); + + i++; + } + + m_modCount = build.mods.size(); + + connect(m_filesNetJob.get(), &NetJob::succeeded, this, &Technic::SolderPackInstallTask::downloadSucceeded); + connect(m_filesNetJob.get(), &NetJob::progress, this, &Technic::SolderPackInstallTask::downloadProgressChanged); + connect(m_filesNetJob.get(), &NetJob::failed, this, &Technic::SolderPackInstallTask::downloadFailed); + m_filesNetJob->start(); +} + +void Technic::SolderPackInstallTask::downloadSucceeded() +{ + m_abortable = false; + + setStatus(tr("Extracting modpack")); + m_filesNetJob.reset(); + m_extractFuture = QtConcurrent::run([this]() + { + int i = 0; + QString extractDir = FS::PathCombine(m_stagingPath, ".minecraft"); + FS::ensureFolderPathExists(extractDir); + + while (m_modCount > i) + { + auto path = FS::PathCombine(m_outputDir.path(), QString("%1").arg(i)); + if (!MMCZip::extractDir(path, extractDir)) + { + return false; + } + i++; + } + return true; + }); + connect(&m_extractFutureWatcher, &QFutureWatcher::finished, this, &Technic::SolderPackInstallTask::extractFinished); + connect(&m_extractFutureWatcher, &QFutureWatcher::canceled, this, &Technic::SolderPackInstallTask::extractAborted); + m_extractFutureWatcher.setFuture(m_extractFuture); +} + +void Technic::SolderPackInstallTask::downloadFailed(QString reason) +{ + m_abortable = false; + emitFailed(reason); + m_filesNetJob.reset(); +} + +void Technic::SolderPackInstallTask::downloadProgressChanged(qint64 current, qint64 total) +{ + m_abortable = true; + setProgress(current / 2, total); +} + +void Technic::SolderPackInstallTask::extractFinished() +{ + if (!m_extractFuture.result()) + { + emitFailed(tr("Failed to extract modpack")); + return; + } + QDir extractDir(m_stagingPath); + + qDebug() << "Fixing permissions for extracted pack files..."; + QDirIterator it(extractDir, QDirIterator::Subdirectories); + while (it.hasNext()) + { + auto filepath = it.next(); + QFileInfo file(filepath); + auto permissions = QFile::permissions(filepath); + auto origPermissions = permissions; + if(file.isDir()) + { + // Folder +rwx for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser | QFileDevice::Permission::ExeUser; + } + else + { + // File +rw for current user + permissions |= QFileDevice::Permission::ReadUser | QFileDevice::Permission::WriteUser; + } + if(origPermissions != permissions) + { + if(!QFile::setPermissions(filepath, permissions)) + { + logWarning(tr("Could not fix permissions for %1").arg(filepath)); + } + else + { + qDebug() << "Fixed" << filepath; + } + } + } + + shared_qobject_ptr packProcessor = new Technic::TechnicPackProcessor(); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::succeeded, this, &Technic::SolderPackInstallTask::emitSucceeded); + connect(packProcessor.get(), &Technic::TechnicPackProcessor::failed, this, &Technic::SolderPackInstallTask::emitFailed); + packProcessor->run(m_globalSettings, m_instName, m_instIcon, m_stagingPath, m_minecraftVersion, true); +} + +void Technic::SolderPackInstallTask::extractAborted() +{ + emitFailed(tr("Instance import has been aborted.")); +} diff --git a/ultimmc/launcher/modplatform/technic/SolderPackInstallTask.h b/ultimmc/launcher/modplatform/technic/SolderPackInstallTask.h new file mode 100644 index 0000000..2159678 --- /dev/null +++ b/ultimmc/launcher/modplatform/technic/SolderPackInstallTask.h @@ -0,0 +1,63 @@ +/* Copyright 2013-2021 MultiMC Contributors + * Copyright 2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +namespace Technic +{ + class SolderPackInstallTask : public InstanceTask + { + Q_OBJECT + public: + explicit SolderPackInstallTask(shared_qobject_ptr network, const QUrl &sourceUrl, const QString& version, const QString &minecraftVersion); + + bool canAbort() const override { return true; } + bool abort() override; + + protected: + //! Entry point for tasks. + virtual void executeTask() override; + + private slots: + void fileListSucceeded(); + void downloadSucceeded(); + void downloadFailed(QString reason); + void downloadProgressChanged(qint64 current, qint64 total); + void extractFinished(); + void extractAborted(); + + private: + bool m_abortable = false; + + shared_qobject_ptr m_network; + + NetJob::Ptr m_filesNetJob; + QUrl m_sourceUrl; + QString m_version; + QString m_minecraftVersion; + QByteArray m_response; + QTemporaryDir m_outputDir; + int m_modCount; + QFuture m_extractFuture; + QFutureWatcher m_extractFutureWatcher; + }; +} diff --git a/ultimmc/launcher/modplatform/technic/SolderPackManifest.cpp b/ultimmc/launcher/modplatform/technic/SolderPackManifest.cpp new file mode 100644 index 0000000..ad195d3 --- /dev/null +++ b/ultimmc/launcher/modplatform/technic/SolderPackManifest.cpp @@ -0,0 +1,56 @@ +/* + * Copyright 2022 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SolderPackManifest.h" + +#include "Json.h" + +namespace TechnicSolder { + +void loadPack(Pack& v, QJsonObject& obj) +{ + v.recommended = Json::requireString(obj, "recommended"); + v.latest = Json::requireString(obj, "latest"); + + auto builds = Json::requireArray(obj, "builds"); + for (const auto buildRaw : builds) { + auto build = Json::requireValueString(buildRaw); + v.builds.append(build); + } +} + +static void loadPackBuildMod(PackBuildMod& b, QJsonObject& obj) +{ + b.name = Json::requireString(obj, "name"); + b.version = Json::requireString(obj, "version"); + b.md5 = Json::requireString(obj, "md5"); + b.url = Json::requireString(obj, "url"); +} + +void loadPackBuild(PackBuild& v, QJsonObject& obj) +{ + v.minecraft = Json::requireString(obj, "minecraft"); + + auto mods = Json::requireArray(obj, "mods"); + for (const auto modRaw : mods) { + auto modObj = Json::requireValueObject(modRaw); + PackBuildMod mod; + loadPackBuildMod(mod, modObj); + v.mods.append(mod); + } +} + +} diff --git a/ultimmc/launcher/modplatform/technic/SolderPackManifest.h b/ultimmc/launcher/modplatform/technic/SolderPackManifest.h new file mode 100644 index 0000000..b4d8cf5 --- /dev/null +++ b/ultimmc/launcher/modplatform/technic/SolderPackManifest.h @@ -0,0 +1,47 @@ +/* + * Copyright 2022 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +namespace TechnicSolder { + +struct Pack { + QString recommended; + QString latest; + QVector builds; +}; + +void loadPack(Pack& v, QJsonObject& obj); + +struct PackBuildMod { + QString name; + QString version; + QString md5; + QString url; +}; + +struct PackBuild { + QString minecraft; + QVector mods; +}; + +void loadPackBuild(PackBuild& v, QJsonObject& obj); + +} diff --git a/ultimmc/launcher/modplatform/technic/TechnicPackProcessor.cpp b/ultimmc/launcher/modplatform/technic/TechnicPackProcessor.cpp new file mode 100644 index 0000000..2d0ea03 --- /dev/null +++ b/ultimmc/launcher/modplatform/technic/TechnicPackProcessor.cpp @@ -0,0 +1,212 @@ +/* Copyright 2020-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "TechnicPackProcessor.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +void Technic::TechnicPackProcessor::run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion, const bool isSolder) +{ + QString minecraftPath = FS::PathCombine(stagingPath, ".minecraft"); + QString configPath = FS::PathCombine(stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(configPath); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + MinecraftInstance instance(globalSettings, instanceSettings, stagingPath); + + instance.setName(instName); + + if (instIcon != "default") + { + instance.setIconKey(instIcon); + } + + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + + QByteArray data; + + QString modpackJar = FS::PathCombine(minecraftPath, "bin", "modpack.jar"); + QString versionJson = FS::PathCombine(minecraftPath, "bin", "version.json"); + QString fmlMinecraftVersion; + if (QFile::exists(modpackJar)) + { + QuaZip zipFile(modpackJar); + if (!zipFile.open(QuaZip::mdUnzip)) + { + emit failed(tr("Unable to open \"bin/modpack.jar\" file!")); + return; + } + QuaZipDir zipFileRoot(&zipFile, "/"); + if (zipFileRoot.exists("/version.json")) + { + if (zipFileRoot.exists("/fmlversion.properties")) + { + zipFile.setCurrentFile("fmlversion.properties"); + QuaZipFile file(&zipFile); + if (!file.open(QIODevice::ReadOnly)) + { + emit failed(tr("Unable to open \"fmlversion.properties\"!")); + return; + } + QByteArray fmlVersionData = file.readAll(); + file.close(); + INIFile iniFile; + iniFile.loadFile(fmlVersionData); + // If not present, this evaluates to a null string + fmlMinecraftVersion = iniFile["fmlbuild.mcversion"].toString(); + } + zipFile.setCurrentFile("version.json", QuaZip::csSensitive); + QuaZipFile file(&zipFile); + if (!file.open(QIODevice::ReadOnly)) + { + emit failed(tr("Unable to open \"version.json\"!")); + return; + } + data = file.readAll(); + file.close(); + } + else + { + if (minecraftVersion.isEmpty()) + emit failed(tr("Could not find \"version.json\" inside \"bin/modpack.jar\", but minecraft version is unknown")); + components->setComponentVersion("net.minecraft", minecraftVersion, true); + components->installJarMods({modpackJar}); + + // Forge for 1.4.7 and for 1.5.2 require extra libraries. + // Figure out the forge version and add it as a component + // (the code still comes from the jar mod installed above) + if (zipFileRoot.exists("/forgeversion.properties")) + { + zipFile.setCurrentFile("forgeversion.properties", QuaZip::csSensitive); + QuaZipFile file(&zipFile); + if (!file.open(QIODevice::ReadOnly)) + { + // Really shouldn't happen, but error handling shall not be forgotten + emit failed(tr("Unable to open \"forgeversion.properties\"")); + return; + } + QByteArray forgeVersionData = file.readAll(); + file.close(); + INIFile iniFile; + iniFile.loadFile(forgeVersionData); + QString major, minor, revision, build; + major = iniFile["forge.major.number"].toString(); + minor = iniFile["forge.minor.number"].toString(); + revision = iniFile["forge.revision.number"].toString(); + build = iniFile["forge.build.number"].toString(); + + if (major.isEmpty() || minor.isEmpty() || revision.isEmpty() || build.isEmpty()) + { + emit failed(tr("Invalid \"forgeversion.properties\"!")); + return; + } + + components->setComponentVersion("net.minecraftforge", major + '.' + minor + '.' + revision + '.' + build); + } + + components->saveNow(); + emit succeeded(); + return; + } + } + else if (QFile::exists(versionJson)) + { + QFile file(versionJson); + if (!file.open(QIODevice::ReadOnly)) + { + emit failed(tr("Unable to open \"version.json\"!")); + return; + } + data = file.readAll(); + file.close(); + } + else + { + // This is the "Vanilla" modpack, excluded by the search code + emit failed(tr("Unable to find a \"version.json\"!")); + return; + } + + try + { + QJsonDocument doc = Json::requireDocument(data); + QJsonObject root = Json::requireObject(doc, "version.json"); + QString minecraftVersion = Json::ensureString(root, "inheritsFrom", QString(), ""); + if (minecraftVersion.isEmpty()) + { + if (fmlMinecraftVersion.isEmpty()) + { + emit failed(tr("Could not understand \"version.json\":\ninheritsFrom is missing")); + return; + } + minecraftVersion = fmlMinecraftVersion; + } + components->setComponentVersion("net.minecraft", minecraftVersion, true); + for (auto library: Json::ensureArray(root, "libraries", {})) + { + if (!library.isObject()) + { + continue; + } + + auto libraryObject = Json::ensureValueObject(library, {}, ""); + auto libraryName = Json::ensureString(libraryObject, "name", "", ""); + + if (libraryName.startsWith("net.minecraftforge:forge:") && libraryName.contains('-')) + { + QString libraryVersion = libraryName.section(':', 2); + if (!libraryVersion.startsWith("1.7.10-")) + { + components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1)); + } + else + { + // 1.7.10 versions sometimes look like 1.7.10-10.13.4.1614-1.7.10, this filters out the 10.13.4.1614 part + components->setComponentVersion("net.minecraftforge", libraryName.section('-', 1, 1)); + } + } + else if (libraryName.startsWith("net.minecraftforge:minecraftforge:")) + { + components->setComponentVersion("net.minecraftforge", libraryName.section(':', 2)); + } + else if (libraryName.startsWith("net.fabricmc:fabric-loader:")) + { + components->setComponentVersion("net.fabricmc.fabric-loader", libraryName.section(':', 2)); + } + else if (libraryName.startsWith("org.quiltmc:quilt-loader:")) + { + components->setComponentVersion("org.quiltmc.quilt-loader", libraryName.section(':', 2)); + } + } + } + catch (const JSONValidationError &e) + { + emit failed(tr("Could not understand \"version.json\":\n") + e.cause()); + return; + } + + components->saveNow(); + emit succeeded(); +} diff --git a/ultimmc/launcher/modplatform/technic/TechnicPackProcessor.h b/ultimmc/launcher/modplatform/technic/TechnicPackProcessor.h new file mode 100644 index 0000000..2ad803b --- /dev/null +++ b/ultimmc/launcher/modplatform/technic/TechnicPackProcessor.h @@ -0,0 +1,35 @@ +/* Copyright 2020-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "settings/SettingsObject.h" + +namespace Technic +{ + // not exporting it, only used in SingleZipPackInstallTask, InstanceImportTask and SolderPackInstallTask + class TechnicPackProcessor : public QObject + { + Q_OBJECT + + signals: + void succeeded(); + void failed(QString reason); + + public: + void run(SettingsObjectPtr globalSettings, const QString &instName, const QString &instIcon, const QString &stagingPath, const QString &minecraftVersion=QString(), const bool isSolder = false); + }; +} diff --git a/ultimmc/launcher/mojang/PackageManifest.cpp b/ultimmc/launcher/mojang/PackageManifest.cpp new file mode 100644 index 0000000..6c03943 --- /dev/null +++ b/ultimmc/launcher/mojang/PackageManifest.cpp @@ -0,0 +1,434 @@ +/* + * Copyright 2020 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include "PackageManifest.h" +#include +#include +#include +#include +#include + +#ifndef Q_OS_WIN32 +#include +#include +#include +#endif + +namespace mojang_files { + +const Hash hash_of_empty_string = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; + +int Path::compare(const Path& rhs) const +{ + auto left_cursor = begin(); + auto left_end = end(); + auto right_cursor = rhs.begin(); + auto right_end = rhs.end(); + + while (left_cursor != left_end && right_cursor != right_end) + { + if(*left_cursor < *right_cursor) + { + return -1; + } + else if(*left_cursor > *right_cursor) + { + return 1; + } + left_cursor++; + right_cursor++; + } + + if(left_cursor == left_end) + { + if(right_cursor == right_end) + { + return 0; + } + return -1; + } + return 1; +} + +void Package::addFile(const Path& path, const File& file) { + addFolder(path.parent_path()); + files[path] = file; +} + +void Package::addFolder(Path folder) { + if(!folder.has_parent_path()) { + return; + } + do { + folders.insert(folder); + folder = folder.parent_path(); + } while(folder.has_parent_path()); +} + +void Package::addLink(const Path& path, const Path& target) { + addFolder(path.parent_path()); + symlinks[path] = target; +} + +void Package::addSource(const FileSource& source) { + sources[source.hash] = source; +} + + +namespace { +void fromJson(QJsonDocument & doc, Package & out) { + std::set seen_paths; + if (!doc.isObject()) + { + throw JSONValidationError("file manifest is not an object"); + } + QJsonObject root = doc.object(); + + auto filesObj = Json::ensureObject(root, "files"); + auto iter = filesObj.begin(); + while (iter != filesObj.end()) + { + Path objectPath = Path(iter.key()); + auto value = iter.value(); + iter++; + if(seen_paths.count(objectPath)) { + throw JSONValidationError("duplicate path inside manifest, the manifest is invalid"); + } + if (!value.isObject()) + { + throw JSONValidationError("file entry inside manifest is not an an object"); + } + seen_paths.insert(objectPath); + + auto fileObject = value.toObject(); + auto type = Json::requireString(fileObject, "type"); + if(type == "directory") { + out.addFolder(objectPath); + continue; + } + else if(type == "file") { + FileSource bestSource; + File file; + file.executable = Json::ensureBoolean(fileObject, QString("executable"), false); + auto downloads = Json::requireObject(fileObject, "downloads"); + for(auto iter2 = downloads.begin(); iter2 != downloads.end(); iter2++) { + FileSource source; + + auto downloadObject = Json::requireValueObject(iter2.value()); + source.hash = Json::requireString(downloadObject, "sha1"); + source.size = Json::requireInteger(downloadObject, "size"); + source.url = Json::requireString(downloadObject, "url"); + + auto compression = iter2.key(); + if(compression == "raw") { + file.hash = source.hash; + file.size = source.size; + source.compression = Compression::Raw; + } + else if (compression == "lzma") { + source.compression = Compression::Lzma; + } + else { + continue; + } + bestSource.upgrade(source); + } + if(bestSource.isBad()) { + throw JSONValidationError("No valid compression method for file " + iter.key()); + } + out.addFile(objectPath, file); + out.addSource(bestSource); + } + else if(type == "link") { + auto target = Json::requireString(fileObject, "target"); + out.symlinks[objectPath] = target; + out.addLink(objectPath, target); + } + else { + throw JSONValidationError("Invalid item type in manifest: " + type); + } + } + // make sure the containing folder exists + out.folders.insert(Path()); +} +} + +Package Package::fromManifestContents(const QByteArray& contents) +{ + Package out; + try + { + auto doc = Json::requireDocument(contents, "Manifest"); + fromJson(doc, out); + return out; + } + catch (const Exception &e) + { + qDebug() << QString("Unable to parse manifest: %1").arg(e.cause()); + out.valid = false; + return out; + } +} + +Package Package::fromManifestFile(const QString & filename) { + Package out; + try + { + auto doc = Json::requireDocument(filename, filename); + fromJson(doc, out); + return out; + } + catch (const Exception &e) + { + qDebug() << QString("Unable to parse manifest file %1: %2").arg(filename, e.cause()); + out.valid = false; + return out; + } +} + +#ifndef Q_OS_WIN32 + +#include +#include +#include + +namespace { +// FIXME: Qt obscures symlink targets by making them absolute. that is useless. this is the workaround - we do it ourselves +bool actually_read_symlink_target(const QString & filepath, Path & out) +{ + struct ::stat st; + // FIXME: here, we assume the native filesystem encoding. May the Gods have mercy upon our Souls. + QByteArray nativePath = filepath.toUtf8(); + const char * filepath_cstr = nativePath.data(); + + if (lstat(filepath_cstr, &st) != 0) + { + return false; + } + + auto size = st.st_size ? st.st_size + 1 : PATH_MAX; + std::string temp(size, '\0'); + // because we don't realiably know how long the damn thing actually is, we loop and expand. POSIX is naff + do + { + auto link_length = ::readlink(filepath_cstr, &temp[0], temp.size()); + if(link_length == -1) + { + return false; + } + if(std::string::size_type(link_length) < temp.size()) + { + // buffer was long enough and we managed to read the link target. RETURN here. + temp.resize(link_length); + out = Path(QString::fromUtf8(temp.c_str())); + return true; + } + temp.resize(temp.size() * 2); + } while (true); +} +} +#endif + +// FIXME: Qt filesystem abstraction is bad, but ... let's hope it doesn't break too much? +// FIXME: The error handling is just DEFICIENT +Package Package::fromInspectedFolder(const QString& folderPath) +{ + QDir root(folderPath); + + Package out; + QDirIterator iterator(folderPath, QDir::NoDotAndDotDot | QDir::AllEntries | QDir::System | QDir::Hidden, QDirIterator::Subdirectories); + while(iterator.hasNext()) { + iterator.next(); + + auto fileInfo = iterator.fileInfo(); + auto relPath = root.relativeFilePath(fileInfo.filePath()); + // FIXME: this is probably completely busted on Windows anyway, so just disable it. + // Qt makes shit up and doesn't understand the platform details + // TODO: Actually use a filesystem library that isn't terrible and has decen license. + // I only know one, and I wrote it. Sadly, currently proprietary. PAIN. +#ifndef Q_OS_WIN32 + if(fileInfo.isSymLink()) { + Path targetPath; + if(!actually_read_symlink_target(fileInfo.filePath(), targetPath)) { + qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath(); + out.valid = false; + } + out.addLink(relPath, targetPath); + } + else +#endif + if(fileInfo.isDir()) { + out.addFolder(relPath); + } + else if(fileInfo.isFile()) { + File f; + f.executable = fileInfo.isExecutable(); + f.size = fileInfo.size(); + // FIXME: async / optimize the hashing + QFile input(fileInfo.absoluteFilePath()); + if(!input.open(QIODevice::ReadOnly)) { + qCritical() << "Folder inspection: Failed to open file:" << fileInfo.absoluteFilePath(); + out.valid = false; + break; + } + f.hash = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Sha1).toHex().constData(); + out.addFile(relPath, f); + } + else { + // Something else... oh my + qCritical() << "Folder inspection: Unknown filesystem object:" << fileInfo.absoluteFilePath(); + out.valid = false; + break; + } + } + out.folders.insert(Path(".")); + out.valid = true; + return out; +} + +namespace { +struct shallow_first_sort +{ + bool operator()(const Path &lhs, const Path &rhs) const + { + auto lhs_depth = lhs.length(); + auto rhs_depth = rhs.length(); + if(lhs_depth < rhs_depth) + { + return true; + } + else if(lhs_depth == rhs_depth) + { + if(lhs < rhs) + { + return true; + } + } + return false; + } +}; + +struct deep_first_sort +{ + bool operator()(const Path &lhs, const Path &rhs) const + { + auto lhs_depth = lhs.length(); + auto rhs_depth = rhs.length(); + if(lhs_depth > rhs_depth) + { + return true; + } + else if(lhs_depth == rhs_depth) + { + if(lhs < rhs) + { + return true; + } + } + return false; + } +}; +} + +UpdateOperations UpdateOperations::resolve(const Package& from, const Package& to) +{ + UpdateOperations out; + + if(!from.valid || !to.valid) { + out.valid = false; + return out; + } + + // Files + for(auto iter = from.files.begin(); iter != from.files.end(); iter++) { + const auto ¤t_hash = iter->second.hash; + const auto ¤t_executable = iter->second.executable; + const auto &path = iter->first; + + auto iter2 = to.files.find(path); + if(iter2 == to.files.end()) { + // removed + out.deletes.push_back(path); + continue; + } + auto new_hash = iter2->second.hash; + auto new_executable = iter2->second.executable; + if (current_hash != new_hash) { + out.deletes.push_back(path); + out.downloads.emplace( + std::pair{ + path, + FileDownload(to.sources.at(iter2->second.hash), iter2->second.executable) + } + ); + } + else if (current_executable != new_executable) { + out.executable_fixes[path] = new_executable; + } + } + for(auto iter = to.files.begin(); iter != to.files.end(); iter++) { + auto path = iter->first; + if(!from.files.count(path)) { + out.downloads.emplace( + std::pair{ + path, + FileDownload(to.sources.at(iter->second.hash), iter->second.executable) + } + ); + } + } + + // Folders + std::set remove_folders; + std::set make_folders; + for(auto from_path: from.folders) { + auto iter = to.folders.find(from_path); + if(iter == to.folders.end()) { + remove_folders.insert(from_path); + } + } + for(auto & rmdir: remove_folders) { + out.rmdirs.push_back(rmdir); + } + for(auto to_path: to.folders) { + auto iter = from.folders.find(to_path); + if(iter == from.folders.end()) { + make_folders.insert(to_path); + } + } + for(auto & mkdir: make_folders) { + out.mkdirs.push_back(mkdir); + } + + // Symlinks + for(auto iter = from.symlinks.begin(); iter != from.symlinks.end(); iter++) { + const auto ¤t_target = iter->second; + const auto &path = iter->first; + + auto iter2 = to.symlinks.find(path); + if(iter2 == to.symlinks.end()) { + // removed + out.deletes.push_back(path); + continue; + } + const auto &new_target = iter2->second; + if (current_target != new_target) { + out.deletes.push_back(path); + out.mklinks[path] = iter2->second; + } + } + for(auto iter = to.symlinks.begin(); iter != to.symlinks.end(); iter++) { + auto path = iter->first; + if(!from.symlinks.count(path)) { + out.mklinks[path] = iter->second; + } + } + out.valid = true; + return out; +} + +} diff --git a/ultimmc/launcher/mojang/PackageManifest.h b/ultimmc/launcher/mojang/PackageManifest.h new file mode 100644 index 0000000..c495ac9 --- /dev/null +++ b/ultimmc/launcher/mojang/PackageManifest.h @@ -0,0 +1,178 @@ +/* + * Copyright 2020 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include +#include +#include +#include +#include "tasks/Task.h" + +namespace mojang_files { + +using Hash = QString; +extern const Hash empty_hash; + +// simple-ish path implementation. assumes always relative and does not allow '..' entries +class Path +{ +public: + using parts_type = QStringList; + + Path() = default; + Path(QString string) { + auto parts_in = string.split('/'); + for(auto & part: parts_in) { + if(part.isEmpty() || part == ".") { + continue; + } + if(part == "..") { + if(parts.size()) { + parts.pop_back(); + } + continue; + } + parts.push_back(part); + } + } + + bool has_parent_path() const + { + return parts.size() > 0; + } + + Path parent_path() const + { + if (parts.empty()) + return Path(); + return Path(parts.begin(), std::prev(parts.end())); + } + + bool empty() const + { + return parts.empty(); + } + + int length() const + { + return parts.length(); + } + + bool operator==(const Path & rhs) const { + return parts == rhs.parts; + } + + bool operator!=(const Path & rhs) const { + return parts != rhs.parts; + } + + inline bool operator<(const Path& rhs) const + { + return compare(rhs) < 0; + } + + parts_type::const_iterator begin() const + { + return parts.begin(); + } + + parts_type::const_iterator end() const + { + return parts.end(); + } + + QString toString() const { + return parts.join("/"); + } + +private: + Path(const parts_type::const_iterator & start, const parts_type::const_iterator & end) { + auto cursor = start; + while(cursor != end) { + parts.push_back(*cursor); + cursor++; + } + } + int compare(const Path& p) const; + + parts_type parts; +}; + + +enum class Compression { + Raw, + Lzma, + Unknown +}; + + +struct FileSource +{ + Compression compression = Compression::Unknown; + Hash hash; + QString url; + std::size_t size = 0; + void upgrade(const FileSource & other) { + if(compression == Compression::Unknown || other.size < size) { + *this = other; + } + } + bool isBad() const { + return compression == Compression::Unknown; + } +}; + +struct File +{ + Hash hash; + bool executable; + std::uint64_t size = 0; +}; + +struct Package { + static Package fromInspectedFolder(const QString &folderPath); + static Package fromManifestFile(const QString &path); + static Package fromManifestContents(const QByteArray& contents); + + explicit operator bool() const + { + return valid; + } + void addFolder(Path folder); + void addFile(const Path & path, const File & file); + void addLink(const Path & path, const Path & target); + void addSource(const FileSource & source); + + std::map sources; + bool valid = true; + std::set folders; + std::map files; + std::map symlinks; +}; + +struct FileDownload : FileSource +{ + FileDownload(const FileSource& source, bool executable) { + static_cast (*this) = source; + this->executable = executable; + } + bool executable = false; +}; + +struct UpdateOperations { + static UpdateOperations resolve(const Package & from, const Package & to); + bool valid = false; + std::vector deletes; + std::vector rmdirs; + std::vector mkdirs; + std::map downloads; + std::map mklinks; + std::map executable_fixes; +}; + +} diff --git a/ultimmc/launcher/mojang/PackageManifest_test.cpp b/ultimmc/launcher/mojang/PackageManifest_test.cpp new file mode 100644 index 0000000..47c7cbb --- /dev/null +++ b/ultimmc/launcher/mojang/PackageManifest_test.cpp @@ -0,0 +1,351 @@ +/* + * Copyright 2020 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include +#include +#include "TestUtil.h" + +#include "mojang/PackageManifest.h" + +using namespace mojang_files; + +QDebug operator<<(QDebug debug, const Path &path) +{ + debug << path.toString(); + return debug; +} + +class PackageManifestTest : public QObject +{ + Q_OBJECT + +private slots: + void test_parse(); + void test_parse_file(); + void test_inspect(); +#ifndef Q_OS_WIN32 + void test_inspect_symlinks(); +#endif + void mkdir_deep(); + void rmdir_deep(); + + void identical_file(); + void changed_file(); + void added_file(); + void removed_file(); +}; + +namespace { +QByteArray basic_manifest = R"END( +{ + "files": { + "a/b.txt": { + "type": "file", + "downloads": { + "raw": { + "url": "http://dethware.org/b.txt", + "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709", + "size": 0 + } + }, + "executable": true + }, + "a/b/c": { + "type": "directory" + }, + "a/b/c.txt": { + "type": "link", + "target": "../b.txt" + } + } +} +)END"; +} + +void PackageManifestTest::test_parse() +{ + auto manifest = Package::fromManifestContents(basic_manifest); + QVERIFY(manifest.valid == true); + QVERIFY(manifest.files.size() == 1); + QVERIFY(manifest.files.count(Path("a/b.txt"))); + auto &file = manifest.files[Path("a/b.txt")]; + QVERIFY(file.executable == true); + QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); + QVERIFY(file.size == 0); + QVERIFY(manifest.folders.size() == 4); + QVERIFY(manifest.folders.count(Path("."))); + QVERIFY(manifest.folders.count(Path("a"))); + QVERIFY(manifest.folders.count(Path("a/b"))); + QVERIFY(manifest.folders.count(Path("a/b/c"))); + QVERIFY(manifest.symlinks.size() == 1); + auto symlinkPath = Path("a/b/c.txt"); + QVERIFY(manifest.symlinks.count(symlinkPath)); + auto &symlink = manifest.symlinks[symlinkPath]; + QVERIFY(symlink == Path("../b.txt")); + QVERIFY(manifest.sources.size() == 1); +} + +void PackageManifestTest::test_parse_file() { + auto path = QFINDTESTDATA("testdata/1.8.0_202-x64.json"); + auto manifest = Package::fromManifestFile(path); + QVERIFY(manifest.valid == true); +} + + +void PackageManifestTest::test_inspect() { + auto path = QFINDTESTDATA("testdata/inspect_win/"); + auto manifest = Package::fromInspectedFolder(path); + QVERIFY(manifest.valid == true); + QVERIFY(manifest.files.size() == 2); + QVERIFY(manifest.files.count(Path("a/b.txt"))); + auto &file1 = manifest.files[Path("a/b.txt")]; + QVERIFY(file1.executable == false); + QVERIFY(file1.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); + QVERIFY(file1.size == 0); + QVERIFY(manifest.files.count(Path("a/b/b.txt"))); + auto &file2 = manifest.files[Path("a/b/b.txt")]; + QVERIFY(file2.executable == false); + QVERIFY(file2.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); + QVERIFY(file2.size == 0); + QVERIFY(manifest.folders.size() == 3); + QVERIFY(manifest.folders.count(Path("."))); + QVERIFY(manifest.folders.count(Path("a"))); + QVERIFY(manifest.folders.count(Path("a/b"))); + QVERIFY(manifest.symlinks.size() == 0); +} + +#ifndef Q_OS_WIN32 +void PackageManifestTest::test_inspect_symlinks() { + auto path = QFINDTESTDATA("testdata/inspect/"); + auto manifest = Package::fromInspectedFolder(path); + QVERIFY(manifest.valid == true); + QVERIFY(manifest.files.size() == 1); + QVERIFY(manifest.files.count(Path("a/b.txt"))); + auto &file = manifest.files[Path("a/b.txt")]; + QVERIFY(file.executable == true); + QVERIFY(file.hash == "da39a3ee5e6b4b0d3255bfef95601890afd80709"); + QVERIFY(file.size == 0); + QVERIFY(manifest.folders.size() == 3); + QVERIFY(manifest.folders.count(Path("."))); + QVERIFY(manifest.folders.count(Path("a"))); + QVERIFY(manifest.folders.count(Path("a/b"))); + QVERIFY(manifest.symlinks.size() == 1); + QVERIFY(manifest.symlinks.count(Path("a/b/b.txt"))); + qDebug() << manifest.symlinks[Path("a/b/b.txt")]; + QVERIFY(manifest.symlinks[Path("a/b/b.txt")] == Path("../b.txt")); +} +#endif + +void PackageManifestTest::mkdir_deep() { + + Package from; + auto to = Package::fromManifestContents(R"END( +{ + "files": { + "a/b/c/d/e": { + "type": "directory" + } + } +} +)END"); + auto operations = UpdateOperations::resolve(from, to); + QVERIFY(operations.deletes.size() == 0); + QVERIFY(operations.rmdirs.size() == 0); + + QVERIFY(operations.mkdirs.size() == 6); + QVERIFY(operations.mkdirs[0] == Path(".")); + QVERIFY(operations.mkdirs[1] == Path("a")); + QVERIFY(operations.mkdirs[2] == Path("a/b")); + QVERIFY(operations.mkdirs[3] == Path("a/b/c")); + QVERIFY(operations.mkdirs[4] == Path("a/b/c/d")); + QVERIFY(operations.mkdirs[5] == Path("a/b/c/d/e")); + + QVERIFY(operations.downloads.size() == 0); + QVERIFY(operations.mklinks.size() == 0); + QVERIFY(operations.executable_fixes.size() == 0); +} + +void PackageManifestTest::rmdir_deep() { + + Package to; + auto from = Package::fromManifestContents(R"END( +{ + "files": { + "a/b/c/d/e": { + "type": "directory" + } + } +} +)END"); + auto operations = UpdateOperations::resolve(from, to); + QVERIFY(operations.deletes.size() == 0); + + QVERIFY(operations.rmdirs.size() == 6); + QVERIFY(operations.rmdirs[0] == Path("a/b/c/d/e")); + QVERIFY(operations.rmdirs[1] == Path("a/b/c/d")); + QVERIFY(operations.rmdirs[2] == Path("a/b/c")); + QVERIFY(operations.rmdirs[3] == Path("a/b")); + QVERIFY(operations.rmdirs[4] == Path("a")); + QVERIFY(operations.rmdirs[5] == Path(".")); + + QVERIFY(operations.mkdirs.size() == 0); + QVERIFY(operations.downloads.size() == 0); + QVERIFY(operations.mklinks.size() == 0); + QVERIFY(operations.executable_fixes.size() == 0); +} + +void PackageManifestTest::identical_file() { + QByteArray manifest = R"END( +{ + "files": { + "a/b/c/d/empty.txt": { + "type": "file", + "downloads": { + "raw": { + "url": "http://dethware.org/empty.txt", + "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709", + "size": 0 + } + }, + "executable": false + } + } +} +)END"; + auto from = Package::fromManifestContents(manifest); + auto to = Package::fromManifestContents(manifest); + auto operations = UpdateOperations::resolve(from, to); + QVERIFY(operations.deletes.size() == 0); + QVERIFY(operations.rmdirs.size() == 0); + QVERIFY(operations.mkdirs.size() == 0); + QVERIFY(operations.downloads.size() == 0); + QVERIFY(operations.mklinks.size() == 0); + QVERIFY(operations.executable_fixes.size() == 0); +} + +void PackageManifestTest::changed_file() { + auto from = Package::fromManifestContents(R"END( +{ + "files": { + "a/b/c/d/file": { + "type": "file", + "downloads": { + "raw": { + "url": "http://dethware.org/empty.txt", + "sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709", + "size": 0 + } + }, + "executable": false + } + } +} +)END"); + auto to = Package::fromManifestContents(R"END( +{ + "files": { + "a/b/c/d/file": { + "type": "file", + "downloads": { + "raw": { + "url": "http://dethware.org/space.txt", + "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46", + "size": 1 + } + }, + "executable": false + } + } +} +)END"); + auto operations = UpdateOperations::resolve(from, to); + QVERIFY(operations.deletes.size() == 1); + QCOMPARE(operations.deletes[0], Path("a/b/c/d/file")); + QVERIFY(operations.rmdirs.size() == 0); + QVERIFY(operations.mkdirs.size() == 0); + QVERIFY(operations.downloads.size() == 1); + QVERIFY(operations.mklinks.size() == 0); + QVERIFY(operations.executable_fixes.size() == 0); +} + +void PackageManifestTest::added_file() { + auto from = Package::fromManifestContents(R"END( +{ + "files": { + "a/b/c/d": { + "type": "directory" + } + } +} +)END"); + auto to = Package::fromManifestContents(R"END( +{ + "files": { + "a/b/c/d/file": { + "type": "file", + "downloads": { + "raw": { + "url": "http://dethware.org/space.txt", + "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46", + "size": 1 + } + }, + "executable": false + } + } +} +)END"); + auto operations = UpdateOperations::resolve(from, to); + QVERIFY(operations.deletes.size() == 0); + QVERIFY(operations.rmdirs.size() == 0); + QVERIFY(operations.mkdirs.size() == 0); + QVERIFY(operations.downloads.size() == 1); + QVERIFY(operations.mklinks.size() == 0); + QVERIFY(operations.executable_fixes.size() == 0); +} + +void PackageManifestTest::removed_file() { + auto from = Package::fromManifestContents(R"END( +{ + "files": { + "a/b/c/d/file": { + "type": "file", + "downloads": { + "raw": { + "url": "http://dethware.org/space.txt", + "sha1": "dd122581c8cd44d0227f9c305581ffcb4b6f1b46", + "size": 1 + } + }, + "executable": false + } + } +} +)END"); + auto to = Package::fromManifestContents(R"END( +{ + "files": { + "a/b/c/d": { + "type": "directory" + } + } +} +)END"); + auto operations = UpdateOperations::resolve(from, to); + QVERIFY(operations.deletes.size() == 1); + QCOMPARE(operations.deletes[0], Path("a/b/c/d/file")); + QVERIFY(operations.rmdirs.size() == 0); + QVERIFY(operations.mkdirs.size() == 0); + QVERIFY(operations.downloads.size() == 0); + QVERIFY(operations.mklinks.size() == 0); + QVERIFY(operations.executable_fixes.size() == 0); +} + +QTEST_GUILESS_MAIN(PackageManifestTest) + +#include "PackageManifest_test.moc" + diff --git a/ultimmc/launcher/mojang/testdata/1.8.0_202-x64.json b/ultimmc/launcher/mojang/testdata/1.8.0_202-x64.json new file mode 100644 index 0000000..3d99d71 --- /dev/null +++ b/ultimmc/launcher/mojang/testdata/1.8.0_202-x64.json @@ -0,0 +1 @@ +{"files": {"COPYRIGHT": {"downloads": {"lzma": {"sha1": "dd860e040807f7e53ae89da5f28dd73d57ac605d", "size": 1431, "url": "https://launcher.mojang.com/v1/objects/dd860e040807f7e53ae89da5f28dd73d57ac605d/COPYRIGHT"}, "raw": {"sha1": "c725183c757011e7ba96c83c1e86ee7e8b516a2b", "size": 3244, "url": "https://launcher.mojang.com/v1/objects/c725183c757011e7ba96c83c1e86ee7e8b516a2b/COPYRIGHT"}}, "executable": false, "type": "file"}, "LICENSE": {"downloads": {"raw": {"sha1": "3e86865deec0814c958bcf7fb87f790bccc0e8bd", "size": 40, "url": "https://launcher.mojang.com/v1/objects/3e86865deec0814c958bcf7fb87f790bccc0e8bd/LICENSE"}}, "executable": false, "type": "file"}, "README": {"downloads": {"raw": {"sha1": "f90331df1e5badeadc501d8dd70714c62a920204", "size": 46, "url": "https://launcher.mojang.com/v1/objects/f90331df1e5badeadc501d8dd70714c62a920204/README"}}, "executable": false, "type": "file"}, "THIRDPARTYLICENSEREADME-JAVAFX.txt": {"downloads": {"lzma": {"sha1": "4fee85109d7ff04b982d0576dabd15397f599125", "size": 15455, "url": "https://launcher.mojang.com/v1/objects/4fee85109d7ff04b982d0576dabd15397f599125/THIRDPARTYLICENSEREADME-JAVAFX.txt"}, "raw": {"sha1": "56ff42f87607b997b52ae0ef8bf315e36932e870", "size": 112724, "url": "https://launcher.mojang.com/v1/objects/56ff42f87607b997b52ae0ef8bf315e36932e870/THIRDPARTYLICENSEREADME-JAVAFX.txt"}}, "executable": false, "type": "file"}, "THIRDPARTYLICENSEREADME.txt": {"downloads": {"lzma": {"sha1": "419c1414ba46ae9dbfd38cf4e0601fff61644429", "size": 32266, "url": "https://launcher.mojang.com/v1/objects/419c1414ba46ae9dbfd38cf4e0601fff61644429/THIRDPARTYLICENSEREADME.txt"}, "raw": {"sha1": "b83c3f32261de3e48ccd20614a11e066b1ec9027", "size": 153824, "url": "https://launcher.mojang.com/v1/objects/b83c3f32261de3e48ccd20614a11e066b1ec9027/THIRDPARTYLICENSEREADME.txt"}}, "executable": false, "type": "file"}, "Welcome.html": {"downloads": {"lzma": {"sha1": "01c21a74b4aafb7cbe0388233c43cbdf77dcaaea", "size": 528, "url": "https://launcher.mojang.com/v1/objects/01c21a74b4aafb7cbe0388233c43cbdf77dcaaea/Welcome.html"}, "raw": {"sha1": "d98ae54f03dac87419abc19b97e315830c2da55f", "size": 955, "url": "https://launcher.mojang.com/v1/objects/d98ae54f03dac87419abc19b97e315830c2da55f/Welcome.html"}}, "executable": false, "type": "file"}, "bin": {"type": "directory"}, "bin/ControlPanel": {"target": "jcontrol", "type": "link"}, "bin/java": {"downloads": {"lzma": {"sha1": "3857eea1d59e1bc545c67a753ed2768254807b8a", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/3857eea1d59e1bc545c67a753ed2768254807b8a/java"}, "raw": {"sha1": "3d20560fb5d1a49cb689c2226972e92e06d27ba6", "size": 8464, "url": "https://launcher.mojang.com/v1/objects/3d20560fb5d1a49cb689c2226972e92e06d27ba6/java"}}, "executable": true, "type": "file"}, "bin/javaws": {"downloads": {"lzma": {"sha1": "a6bec5c049e76c4488294a256a2084ea23ddb440", "size": 38173, "url": "https://launcher.mojang.com/v1/objects/a6bec5c049e76c4488294a256a2084ea23ddb440/javaws"}, "raw": {"sha1": "955c0f0066e2f893b0c2b3ccd83e223722e4ab74", "size": 140296, "url": "https://launcher.mojang.com/v1/objects/955c0f0066e2f893b0c2b3ccd83e223722e4ab74/javaws"}}, "executable": true, "type": "file"}, "bin/jcontrol": {"downloads": {"lzma": {"sha1": "40c5e33748f252e1d950b579a4185ab2c23fc908", "size": 2166, "url": "https://launcher.mojang.com/v1/objects/40c5e33748f252e1d950b579a4185ab2c23fc908/jcontrol"}, "raw": {"sha1": "ed541733c8b51e34349c1f8010b277e58ad73f1e", "size": 6264, "url": "https://launcher.mojang.com/v1/objects/ed541733c8b51e34349c1f8010b277e58ad73f1e/jcontrol"}}, "executable": true, "type": "file"}, "bin/jjs": {"downloads": {"lzma": {"sha1": "d44d1ac421979f7671921986214812095a5b0e3b", "size": 2168, "url": "https://launcher.mojang.com/v1/objects/d44d1ac421979f7671921986214812095a5b0e3b/jjs"}, "raw": {"sha1": "f00f944c3dbe556793b5dc686aaeee3e5722e99b", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/f00f944c3dbe556793b5dc686aaeee3e5722e99b/jjs"}}, "executable": true, "type": "file"}, "bin/keytool": {"downloads": {"lzma": {"sha1": "93c607dce450976667c382f609a367167bdec05c", "size": 2175, "url": "https://launcher.mojang.com/v1/objects/93c607dce450976667c382f609a367167bdec05c/keytool"}, "raw": {"sha1": "7114b561546270e441e9ed1bcc24e5188c068a42", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/7114b561546270e441e9ed1bcc24e5188c068a42/keytool"}}, "executable": true, "type": "file"}, "bin/orbd": {"downloads": {"lzma": {"sha1": "b27dfded5e2b2f6f02c555971c94e46ca14ac81b", "size": 2254, "url": "https://launcher.mojang.com/v1/objects/b27dfded5e2b2f6f02c555971c94e46ca14ac81b/orbd"}, "raw": {"sha1": "7f31217fecb3dbbd89f1dd3783fca58793a66fd2", "size": 8656, "url": "https://launcher.mojang.com/v1/objects/7f31217fecb3dbbd89f1dd3783fca58793a66fd2/orbd"}}, "executable": true, "type": "file"}, "bin/pack200": {"downloads": {"lzma": {"sha1": "b52da4497b49b1508b6225a5740857ddb8f52e97", "size": 2183, "url": "https://launcher.mojang.com/v1/objects/b52da4497b49b1508b6225a5740857ddb8f52e97/pack200"}, "raw": {"sha1": "16ef3e801efb57e50bc6477a27a9d95d02d0775b", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/16ef3e801efb57e50bc6477a27a9d95d02d0775b/pack200"}}, "executable": true, "type": "file"}, "bin/policytool": {"downloads": {"lzma": {"sha1": "87da4c07da45f3d1a1a9d732af197cd39bf69d10", "size": 2182, "url": "https://launcher.mojang.com/v1/objects/87da4c07da45f3d1a1a9d732af197cd39bf69d10/policytool"}, "raw": {"sha1": "a52a29424470cb9b8db5c2fb1751d0b697a7ec8e", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/a52a29424470cb9b8db5c2fb1751d0b697a7ec8e/policytool"}}, "executable": true, "type": "file"}, "bin/rmid": {"downloads": {"lzma": {"sha1": "1494c1174fde0c0a93ea117bc7edf7eb936c0512", "size": 2172, "url": "https://launcher.mojang.com/v1/objects/1494c1174fde0c0a93ea117bc7edf7eb936c0512/rmid"}, "raw": {"sha1": "5c8710e1ab924e5b09a07bcb4c6e106293bbd1a8", "size": 8584, "url": "https://launcher.mojang.com/v1/objects/5c8710e1ab924e5b09a07bcb4c6e106293bbd1a8/rmid"}}, "executable": true, "type": "file"}, "bin/rmiregistry": {"downloads": {"lzma": {"sha1": "7070cf2ec5a5e520a880bae699431edf02083e7e", "size": 2174, "url": "https://launcher.mojang.com/v1/objects/7070cf2ec5a5e520a880bae699431edf02083e7e/rmiregistry"}, "raw": {"sha1": "5f518daa7050028d5d9d849634c73136f2b23a54", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/5f518daa7050028d5d9d849634c73136f2b23a54/rmiregistry"}}, "executable": true, "type": "file"}, "bin/servertool": {"downloads": {"lzma": {"sha1": "1db683a11cc9b7313426c84412f4d95be2fa7ccd", "size": 2185, "url": "https://launcher.mojang.com/v1/objects/1db683a11cc9b7313426c84412f4d95be2fa7ccd/servertool"}, "raw": {"sha1": "49d0ebfeb265ce5a8733e1014541ea2525674a60", "size": 8592, "url": "https://launcher.mojang.com/v1/objects/49d0ebfeb265ce5a8733e1014541ea2525674a60/servertool"}}, "executable": true, "type": "file"}, "bin/tnameserv": {"downloads": {"lzma": {"sha1": "36da9c9a2c5a8b662a3f8d52ca67339bce1c2714", "size": 2291, "url": "https://launcher.mojang.com/v1/objects/36da9c9a2c5a8b662a3f8d52ca67339bce1c2714/tnameserv"}, "raw": {"sha1": "09d998f8efcb6f55d0d87f59e08f8b89662796d9", "size": 8656, "url": "https://launcher.mojang.com/v1/objects/09d998f8efcb6f55d0d87f59e08f8b89662796d9/tnameserv"}}, "executable": true, "type": "file"}, "bin/unpack200": {"downloads": {"lzma": {"sha1": "344959e32fc7ee19eebe7b3cf5ab6d1a7d6641f2", "size": 79721, "url": "https://launcher.mojang.com/v1/objects/344959e32fc7ee19eebe7b3cf5ab6d1a7d6641f2/unpack200"}, "raw": {"sha1": "5dd933132f1b202e19e0c8e093f7113711cfdfc1", "size": 182616, "url": "https://launcher.mojang.com/v1/objects/5dd933132f1b202e19e0c8e093f7113711cfdfc1/unpack200"}}, "executable": true, "type": "file"}, "lib": {"type": "directory"}, "lib/amd64": {"type": "directory"}, "lib/amd64/jli": {"type": "directory"}, "lib/amd64/jli/libjli.so": {"downloads": {"lzma": {"sha1": "372331ee8e375888f798a2e88180a94493e141b0", "size": 48327, "url": "https://launcher.mojang.com/v1/objects/372331ee8e375888f798a2e88180a94493e141b0/libjli.so"}, "raw": {"sha1": "73b0cf8b7415686bc40c561ff77ff2740ccf7a44", "size": 108616, "url": "https://launcher.mojang.com/v1/objects/73b0cf8b7415686bc40c561ff77ff2740ccf7a44/libjli.so"}}, "executable": true, "type": "file"}, "lib/amd64/jvm.cfg": {"downloads": {"lzma": {"sha1": "86bcfebec37b38415525ffd77d3eaf70d0b1b4ca", "size": 435, "url": "https://launcher.mojang.com/v1/objects/86bcfebec37b38415525ffd77d3eaf70d0b1b4ca/jvm.cfg"}, "raw": {"sha1": "84b38bdc745de446ba0ca0232ea3aaf2efd721da", "size": 627, "url": "https://launcher.mojang.com/v1/objects/84b38bdc745de446ba0ca0232ea3aaf2efd721da/jvm.cfg"}}, "executable": false, "type": "file"}, "lib/amd64/libavplugin-53.so": {"downloads": {"lzma": {"sha1": "a332366762d9efc7b845a682b7edce62db44618c", "size": 14747, "url": "https://launcher.mojang.com/v1/objects/a332366762d9efc7b845a682b7edce62db44618c/libavplugin-53.so"}, "raw": {"sha1": "9bd1473dd8a0dc7950c7af1cc69a45548df26eb5", "size": 51720, "url": "https://launcher.mojang.com/v1/objects/9bd1473dd8a0dc7950c7af1cc69a45548df26eb5/libavplugin-53.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-54.so": {"downloads": {"lzma": {"sha1": "2c615852a0720a275163e00597c1f711f11341da", "size": 15153, "url": "https://launcher.mojang.com/v1/objects/2c615852a0720a275163e00597c1f711f11341da/libavplugin-54.so"}, "raw": {"sha1": "8808050c5949c4800b42d1b19b1f8b0d120bcacb", "size": 51768, "url": "https://launcher.mojang.com/v1/objects/8808050c5949c4800b42d1b19b1f8b0d120bcacb/libavplugin-54.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-55.so": {"downloads": {"lzma": {"sha1": "39ee8e7fe14f0010c78973962800f539c3e4c16b", "size": 15168, "url": "https://launcher.mojang.com/v1/objects/39ee8e7fe14f0010c78973962800f539c3e4c16b/libavplugin-55.so"}, "raw": {"sha1": "f10ea4ea3489e96d8d161a96790133c417ec44e1", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/f10ea4ea3489e96d8d161a96790133c417ec44e1/libavplugin-55.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-56.so": {"downloads": {"lzma": {"sha1": "abe7feced5a559f1bdc868526dc69484e0e591a0", "size": 15169, "url": "https://launcher.mojang.com/v1/objects/abe7feced5a559f1bdc868526dc69484e0e591a0/libavplugin-56.so"}, "raw": {"sha1": "e5bfcbff5a5a5a5993a3e689a05ef358c131a3ed", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/e5bfcbff5a5a5a5993a3e689a05ef358c131a3ed/libavplugin-56.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-57.so": {"downloads": {"lzma": {"sha1": "4dd26b4ef2294b6929dcb2c7546b47eac5cc78a9", "size": 15174, "url": "https://launcher.mojang.com/v1/objects/4dd26b4ef2294b6929dcb2c7546b47eac5cc78a9/libavplugin-57.so"}, "raw": {"sha1": "2949e7ff9b0ac90e8943c211cff141ab12eec3f8", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/2949e7ff9b0ac90e8943c211cff141ab12eec3f8/libavplugin-57.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-ffmpeg-56.so": {"downloads": {"lzma": {"sha1": "c688ba1cfa442bf18bee43b2fa870b4dc1ce3fb6", "size": 15231, "url": "https://launcher.mojang.com/v1/objects/c688ba1cfa442bf18bee43b2fa870b4dc1ce3fb6/libavplugin-ffmpeg-56.so"}, "raw": {"sha1": "0d36c971a9ad99fc2292092fdec3a4179b1021b9", "size": 51920, "url": "https://launcher.mojang.com/v1/objects/0d36c971a9ad99fc2292092fdec3a4179b1021b9/libavplugin-ffmpeg-56.so"}}, "executable": true, "type": "file"}, "lib/amd64/libavplugin-ffmpeg-57.so": {"downloads": {"lzma": {"sha1": "087426bdbffebcfa372a438e863785f4ffbe9a6b", "size": 15180, "url": "https://launcher.mojang.com/v1/objects/087426bdbffebcfa372a438e863785f4ffbe9a6b/libavplugin-ffmpeg-57.so"}, "raw": {"sha1": "5e9c4eb4b49eb8e57c01003ec73a1eb8d6d8c462", "size": 51784, "url": "https://launcher.mojang.com/v1/objects/5e9c4eb4b49eb8e57c01003ec73a1eb8d6d8c462/libavplugin-ffmpeg-57.so"}}, "executable": true, "type": "file"}, "lib/amd64/libawt.so": {"downloads": {"lzma": {"sha1": "018be58b205b73c842a55df811b70d0e8237216e", "size": 195720, "url": "https://launcher.mojang.com/v1/objects/018be58b205b73c842a55df811b70d0e8237216e/libawt.so"}, "raw": {"sha1": "02632cd326e3161c00a7e784599dd7b9ee053dce", "size": 759184, "url": "https://launcher.mojang.com/v1/objects/02632cd326e3161c00a7e784599dd7b9ee053dce/libawt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libawt_headless.so": {"downloads": {"lzma": {"sha1": "7ac2517cff75d4bbb0a0412a9b5f18c74ea188fa", "size": 11211, "url": "https://launcher.mojang.com/v1/objects/7ac2517cff75d4bbb0a0412a9b5f18c74ea188fa/libawt_headless.so"}, "raw": {"sha1": "862157ec957008d0911c5daedc004b3a202623a4", "size": 39176, "url": "https://launcher.mojang.com/v1/objects/862157ec957008d0911c5daedc004b3a202623a4/libawt_headless.so"}}, "executable": true, "type": "file"}, "lib/amd64/libawt_xawt.so": {"downloads": {"lzma": {"sha1": "d536a96af27dfe35de6bb2c8759d51c488cdd8d4", "size": 149598, "url": "https://launcher.mojang.com/v1/objects/d536a96af27dfe35de6bb2c8759d51c488cdd8d4/libawt_xawt.so"}, "raw": {"sha1": "28232b3e01b6f11bfe098bfc6eafc3a513dcebf1", "size": 470232, "url": "https://launcher.mojang.com/v1/objects/28232b3e01b6f11bfe098bfc6eafc3a513dcebf1/libawt_xawt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libbci.so": {"downloads": {"lzma": {"sha1": "c36fad091d11e64c815d5ca17c0ef7a55b0776b1", "size": 3509, "url": "https://launcher.mojang.com/v1/objects/c36fad091d11e64c815d5ca17c0ef7a55b0776b1/libbci.so"}, "raw": {"sha1": "33824051db1ccb6332e22c2b63231055240d0af0", "size": 12760, "url": "https://launcher.mojang.com/v1/objects/33824051db1ccb6332e22c2b63231055240d0af0/libbci.so"}}, "executable": true, "type": "file"}, "lib/amd64/libdcpr.so": {"downloads": {"lzma": {"sha1": "70c6b0933a37f2b1124d6e7c131039241fe796ee", "size": 75969, "url": "https://launcher.mojang.com/v1/objects/70c6b0933a37f2b1124d6e7c131039241fe796ee/libdcpr.so"}, "raw": {"sha1": "fa7001bc5d80579e2716590f3eee8027da0beae7", "size": 204456, "url": "https://launcher.mojang.com/v1/objects/fa7001bc5d80579e2716590f3eee8027da0beae7/libdcpr.so"}}, "executable": true, "type": "file"}, "lib/amd64/libdecora_sse.so": {"downloads": {"lzma": {"sha1": "514acc017dfb6cefaf8cc6d18006ce55781cc9bc", "size": 24397, "url": "https://launcher.mojang.com/v1/objects/514acc017dfb6cefaf8cc6d18006ce55781cc9bc/libdecora_sse.so"}, "raw": {"sha1": "d0c84233504c916e548e29f513e25f6a7479abfc", "size": 74912, "url": "https://launcher.mojang.com/v1/objects/d0c84233504c916e548e29f513e25f6a7479abfc/libdecora_sse.so"}}, "executable": true, "type": "file"}, "lib/amd64/libdeploy.so": {"downloads": {"lzma": {"sha1": "6cf31fd98301c749ac0d2c7825f6d925a4409760", "size": 168999, "url": "https://launcher.mojang.com/v1/objects/6cf31fd98301c749ac0d2c7825f6d925a4409760/libdeploy.so"}, "raw": {"sha1": "b3832e97ed8ca794884b56a591b83d02a2c0c06f", "size": 642368, "url": "https://launcher.mojang.com/v1/objects/b3832e97ed8ca794884b56a591b83d02a2c0c06f/libdeploy.so"}}, "executable": true, "type": "file"}, "lib/amd64/libdt_socket.so": {"downloads": {"lzma": {"sha1": "4cc5c880dbb6fa180436d12d60f0abec8ebb59dc", "size": 7784, "url": "https://launcher.mojang.com/v1/objects/4cc5c880dbb6fa180436d12d60f0abec8ebb59dc/libdt_socket.so"}, "raw": {"sha1": "91ce96f252b8139fc12f0f224ed5b1a041767ab7", "size": 24616, "url": "https://launcher.mojang.com/v1/objects/91ce96f252b8139fc12f0f224ed5b1a041767ab7/libdt_socket.so"}}, "executable": true, "type": "file"}, "lib/amd64/libfontmanager.so": {"downloads": {"lzma": {"sha1": "f94e5e94c71c603ff4d3cd1e7e3d9e181fcc145d", "size": 146951, "url": "https://launcher.mojang.com/v1/objects/f94e5e94c71c603ff4d3cd1e7e3d9e181fcc145d/libfontmanager.so"}, "raw": {"sha1": "2428e805f2c53d1283a033dfd11a86fbb7bd7159", "size": 490672, "url": "https://launcher.mojang.com/v1/objects/2428e805f2c53d1283a033dfd11a86fbb7bd7159/libfontmanager.so"}}, "executable": true, "type": "file"}, "lib/amd64/libfxplugins.so": {"downloads": {"lzma": {"sha1": "a640143365d382a5ad743a784bc2f3706d9d6d67", "size": 50048, "url": "https://launcher.mojang.com/v1/objects/a640143365d382a5ad743a784bc2f3706d9d6d67/libfxplugins.so"}, "raw": {"sha1": "0fd4ac04a84c131f1aaee9e6b0898ff9ea69e3ee", "size": 151448, "url": "https://launcher.mojang.com/v1/objects/0fd4ac04a84c131f1aaee9e6b0898ff9ea69e3ee/libfxplugins.so"}}, "executable": true, "type": "file"}, "lib/amd64/libglass.so": {"downloads": {"lzma": {"sha1": "f1ff517714fa5f2c861f33b32db823fe851541f1", "size": 2856, "url": "https://launcher.mojang.com/v1/objects/f1ff517714fa5f2c861f33b32db823fe851541f1/libglass.so"}, "raw": {"sha1": "e7f4fece30ac727be8148d33b8256abd3a41cef9", "size": 13072, "url": "https://launcher.mojang.com/v1/objects/e7f4fece30ac727be8148d33b8256abd3a41cef9/libglass.so"}}, "executable": true, "type": "file"}, "lib/amd64/libglassgtk2.so": {"downloads": {"lzma": {"sha1": "15b90f7a2baacd15e80aa9785d87cf1e4258376d", "size": 220476, "url": "https://launcher.mojang.com/v1/objects/15b90f7a2baacd15e80aa9785d87cf1e4258376d/libglassgtk2.so"}, "raw": {"sha1": "e30a634c2ff2143bdee512360553d6e0304f33b2", "size": 844984, "url": "https://launcher.mojang.com/v1/objects/e30a634c2ff2143bdee512360553d6e0304f33b2/libglassgtk2.so"}}, "executable": true, "type": "file"}, "lib/amd64/libglassgtk3.so": {"downloads": {"lzma": {"sha1": "868c231165f8c9043b7f0e7de208ec023f06a6e7", "size": 220560, "url": "https://launcher.mojang.com/v1/objects/868c231165f8c9043b7f0e7de208ec023f06a6e7/libglassgtk3.so"}, "raw": {"sha1": "762a11a2b376b7b5a2a7cad780715524fdd176d5", "size": 845304, "url": "https://launcher.mojang.com/v1/objects/762a11a2b376b7b5a2a7cad780715524fdd176d5/libglassgtk3.so"}}, "executable": true, "type": "file"}, "lib/amd64/libglib-lite.so": {"downloads": {"lzma": {"sha1": "61b8871242febe1be262de167dc20ae94bf964b4", "size": 457046, "url": "https://launcher.mojang.com/v1/objects/61b8871242febe1be262de167dc20ae94bf964b4/libglib-lite.so"}, "raw": {"sha1": "63afa060fc3f120af76128e51d32603fc4336fa8", "size": 1538352, "url": "https://launcher.mojang.com/v1/objects/63afa060fc3f120af76128e51d32603fc4336fa8/libglib-lite.so"}}, "executable": true, "type": "file"}, "lib/amd64/libgstreamer-lite.so": {"downloads": {"lzma": {"sha1": "2447dc368406ba1b989a29937d41924620e01988", "size": 673056, "url": "https://launcher.mojang.com/v1/objects/2447dc368406ba1b989a29937d41924620e01988/libgstreamer-lite.so"}, "raw": {"sha1": "5505e7ca592ac64371d3db8fe53bcb602e9723d3", "size": 2263872, "url": "https://launcher.mojang.com/v1/objects/5505e7ca592ac64371d3db8fe53bcb602e9723d3/libgstreamer-lite.so"}}, "executable": true, "type": "file"}, "lib/amd64/libhprof.so": {"downloads": {"lzma": {"sha1": "94a5589c818db1fb1cf1881e24e217c309fce2e4", "size": 64471, "url": "https://launcher.mojang.com/v1/objects/94a5589c818db1fb1cf1881e24e217c309fce2e4/libhprof.so"}, "raw": {"sha1": "4bb9bdeef6133b6dd558d52d691b077c03e9b0ee", "size": 175504, "url": "https://launcher.mojang.com/v1/objects/4bb9bdeef6133b6dd558d52d691b077c03e9b0ee/libhprof.so"}}, "executable": true, "type": "file"}, "lib/amd64/libinstrument.so": {"downloads": {"lzma": {"sha1": "84ffea356caf725b42c86a8ebc9587f477ddde29", "size": 18603, "url": "https://launcher.mojang.com/v1/objects/84ffea356caf725b42c86a8ebc9587f477ddde29/libinstrument.so"}, "raw": {"sha1": "cb8009769601e3fecd7ea2b36c344f737b1a9da7", "size": 51560, "url": "https://launcher.mojang.com/v1/objects/cb8009769601e3fecd7ea2b36c344f737b1a9da7/libinstrument.so"}}, "executable": true, "type": "file"}, "lib/amd64/libj2gss.so": {"downloads": {"lzma": {"sha1": "4b2aa699506b126098b585a9617ce1c05707fa29", "size": 14132, "url": "https://launcher.mojang.com/v1/objects/4b2aa699506b126098b585a9617ce1c05707fa29/libj2gss.so"}, "raw": {"sha1": "cbce4a302b255d4d1924ef7606f038af766c5e86", "size": 47688, "url": "https://launcher.mojang.com/v1/objects/cbce4a302b255d4d1924ef7606f038af766c5e86/libj2gss.so"}}, "executable": true, "type": "file"}, "lib/amd64/libj2pcsc.so": {"downloads": {"lzma": {"sha1": "2361d3b2e3da48593c391b29b0d2b5409e4c55e5", "size": 5074, "url": "https://launcher.mojang.com/v1/objects/2361d3b2e3da48593c391b29b0d2b5409e4c55e5/libj2pcsc.so"}, "raw": {"sha1": "1274178492e7a3e997e12f67794616f7c3d8d0b9", "size": 18296, "url": "https://launcher.mojang.com/v1/objects/1274178492e7a3e997e12f67794616f7c3d8d0b9/libj2pcsc.so"}}, "executable": true, "type": "file"}, "lib/amd64/libj2pkcs11.so": {"downloads": {"lzma": {"sha1": "ef927e2790ba05931d0f0bdd63da3d275a834946", "size": 21573, "url": "https://launcher.mojang.com/v1/objects/ef927e2790ba05931d0f0bdd63da3d275a834946/libj2pkcs11.so"}, "raw": {"sha1": "bd4f2af9bfdc6168633d1920c1a1415de06bb45a", "size": 79472, "url": "https://launcher.mojang.com/v1/objects/bd4f2af9bfdc6168633d1920c1a1415de06bb45a/libj2pkcs11.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjaas_unix.so": {"downloads": {"lzma": {"sha1": "7f7e843544ee1eb1454a5826bdd4218685b79430", "size": 2404, "url": "https://launcher.mojang.com/v1/objects/7f7e843544ee1eb1454a5826bdd4218685b79430/libjaas_unix.so"}, "raw": {"sha1": "4c517925c7d464a5b719898eb0bea1b04df31f1f", "size": 8192, "url": "https://launcher.mojang.com/v1/objects/4c517925c7d464a5b719898eb0bea1b04df31f1f/libjaas_unix.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjava.so": {"downloads": {"lzma": {"sha1": "5eee7a42600a44a8bb8d6d7f510fd96a29637ac0", "size": 63113, "url": "https://launcher.mojang.com/v1/objects/5eee7a42600a44a8bb8d6d7f510fd96a29637ac0/libjava.so"}, "raw": {"sha1": "e280aeddf3fc0ec664aef7efc0e0e197a54aaf02", "size": 227672, "url": "https://launcher.mojang.com/v1/objects/e280aeddf3fc0ec664aef7efc0e0e197a54aaf02/libjava.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjava_crw_demo.so": {"downloads": {"lzma": {"sha1": "b197cf23ae3556eb0b45c663f0a8cb62408b961e", "size": 10412, "url": "https://launcher.mojang.com/v1/objects/b197cf23ae3556eb0b45c663f0a8cb62408b961e/libjava_crw_demo.so"}, "raw": {"sha1": "18f20f906977c90d0090b41dbda8dd5cfead5a4c", "size": 26144, "url": "https://launcher.mojang.com/v1/objects/18f20f906977c90d0090b41dbda8dd5cfead5a4c/libjava_crw_demo.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_font.so": {"downloads": {"lzma": {"sha1": "ffbba0e5022f829412b86063d8a90f95f16709b1", "size": 5608, "url": "https://launcher.mojang.com/v1/objects/ffbba0e5022f829412b86063d8a90f95f16709b1/libjavafx_font.so"}, "raw": {"sha1": "8634a0aca612fc40420a4a7cc8af4cc46cfc6725", "size": 17104, "url": "https://launcher.mojang.com/v1/objects/8634a0aca612fc40420a4a7cc8af4cc46cfc6725/libjavafx_font.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_font_freetype.so": {"downloads": {"lzma": {"sha1": "310271eda8a2ac264ffc3640a9d847b49438d0bd", "size": 6942, "url": "https://launcher.mojang.com/v1/objects/310271eda8a2ac264ffc3640a9d847b49438d0bd/libjavafx_font_freetype.so"}, "raw": {"sha1": "3e7572d047c12ba2bc43acec7f98a67c20af8042", "size": 27616, "url": "https://launcher.mojang.com/v1/objects/3e7572d047c12ba2bc43acec7f98a67c20af8042/libjavafx_font_freetype.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_font_pango.so": {"downloads": {"lzma": {"sha1": "a7bcf0669e70b0f43099a99c81e6b6440cb40ac0", "size": 5820, "url": "https://launcher.mojang.com/v1/objects/a7bcf0669e70b0f43099a99c81e6b6440cb40ac0/libjavafx_font_pango.so"}, "raw": {"sha1": "f0b775cc9a514c7ee8b4d6fb300653ce548caf10", "size": 25560, "url": "https://launcher.mojang.com/v1/objects/f0b775cc9a514c7ee8b4d6fb300653ce548caf10/libjavafx_font_pango.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_font_t2k.so": {"downloads": {"lzma": {"sha1": "551c29dc7c7fc83223aa36a6187f7e0c5d650538", "size": 431450, "url": "https://launcher.mojang.com/v1/objects/551c29dc7c7fc83223aa36a6187f7e0c5d650538/libjavafx_font_t2k.so"}, "raw": {"sha1": "91e5813057c3b852d411540160f8ad05fb9f1ed3", "size": 1486128, "url": "https://launcher.mojang.com/v1/objects/91e5813057c3b852d411540160f8ad05fb9f1ed3/libjavafx_font_t2k.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjavafx_iio.so": {"downloads": {"lzma": {"sha1": "c832998fd5e06ed6dcd6428816194c350785420c", "size": 101479, "url": "https://launcher.mojang.com/v1/objects/c832998fd5e06ed6dcd6428816194c350785420c/libjavafx_iio.so"}, "raw": {"sha1": "dcdf68cb25677b76c1cf0bb94294e6e9880a6678", "size": 256336, "url": "https://launcher.mojang.com/v1/objects/dcdf68cb25677b76c1cf0bb94294e6e9880a6678/libjavafx_iio.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjawt.so": {"downloads": {"lzma": {"sha1": "c1ced6aad5c69ff444dc67d0fd7e333558953831", "size": 1872, "url": "https://launcher.mojang.com/v1/objects/c1ced6aad5c69ff444dc67d0fd7e333558953831/libjawt.so"}, "raw": {"sha1": "c5032f2c6fa40bea24e56605cf76b26a27e87b67", "size": 8048, "url": "https://launcher.mojang.com/v1/objects/c5032f2c6fa40bea24e56605cf76b26a27e87b67/libjawt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjdwp.so": {"downloads": {"lzma": {"sha1": "c1aabbb3f5a624b9ad10ed871a1d83510a99b646", "size": 94884, "url": "https://launcher.mojang.com/v1/objects/c1aabbb3f5a624b9ad10ed871a1d83510a99b646/libjdwp.so"}, "raw": {"sha1": "a043e97be47937f6f552e94cf79c76c1c57f9594", "size": 272248, "url": "https://launcher.mojang.com/v1/objects/a043e97be47937f6f552e94cf79c76c1c57f9594/libjdwp.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjfr.so": {"downloads": {"lzma": {"sha1": "11b8e6bfffdccbacbf9dd29dea4b90b753f3c1b7", "size": 8780, "url": "https://launcher.mojang.com/v1/objects/11b8e6bfffdccbacbf9dd29dea4b90b753f3c1b7/libjfr.so"}, "raw": {"sha1": "312392dd186b11c418183e818f1928e8685a07e5", "size": 28384, "url": "https://launcher.mojang.com/v1/objects/312392dd186b11c418183e818f1928e8685a07e5/libjfr.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjfxmedia.so": {"downloads": {"lzma": {"sha1": "a4e7a126eb648ce6e5e6dc151831da37d8334139", "size": 391897, "url": "https://launcher.mojang.com/v1/objects/a4e7a126eb648ce6e5e6dc151831da37d8334139/libjfxmedia.so"}, "raw": {"sha1": "5fa54944327a6012c3d34cb5c1c4432762178dc8", "size": 1636376, "url": "https://launcher.mojang.com/v1/objects/5fa54944327a6012c3d34cb5c1c4432762178dc8/libjfxmedia.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjfxwebkit.so": {"downloads": {"lzma": {"sha1": "b274debd222cdcc2ee84160ebb95144b3880bc97", "size": 20492825, "url": "https://launcher.mojang.com/v1/objects/b274debd222cdcc2ee84160ebb95144b3880bc97/libjfxwebkit.so"}, "raw": {"sha1": "ecee564c3b2f645131b35bb3004abd4caeabd291", "size": 91014584, "url": "https://launcher.mojang.com/v1/objects/ecee564c3b2f645131b35bb3004abd4caeabd291/libjfxwebkit.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjpeg.so": {"downloads": {"lzma": {"sha1": "9ad55e370c5eaaa73c3158339db3c368b1aaf0cb", "size": 113072, "url": "https://launcher.mojang.com/v1/objects/9ad55e370c5eaaa73c3158339db3c368b1aaf0cb/libjpeg.so"}, "raw": {"sha1": "651e6d53ae67db1f0efbf7f104447a9b49b7e333", "size": 292520, "url": "https://launcher.mojang.com/v1/objects/651e6d53ae67db1f0efbf7f104447a9b49b7e333/libjpeg.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjsdt.so": {"downloads": {"lzma": {"sha1": "04b6d1361a34c496b5f652b2477784d69b8b6baf", "size": 3964, "url": "https://launcher.mojang.com/v1/objects/04b6d1361a34c496b5f652b2477784d69b8b6baf/libjsdt.so"}, "raw": {"sha1": "82b48a82bf6183d34cf00a0f81661b45c616f31b", "size": 12904, "url": "https://launcher.mojang.com/v1/objects/82b48a82bf6183d34cf00a0f81661b45c616f31b/libjsdt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjsig.so": {"downloads": {"lzma": {"sha1": "37d3b89abde397216cc4ecb1339d8543d99b8428", "size": 3536, "url": "https://launcher.mojang.com/v1/objects/37d3b89abde397216cc4ecb1339d8543d99b8428/libjsig.so"}, "raw": {"sha1": "42e52ba1bcbe0362ab24bcf65c93797354db6fb9", "size": 13336, "url": "https://launcher.mojang.com/v1/objects/42e52ba1bcbe0362ab24bcf65c93797354db6fb9/libjsig.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjsound.so": {"downloads": {"lzma": {"sha1": "7e3c565d74d8ffae716f32b05544fa4a6f108adc", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7e3c565d74d8ffae716f32b05544fa4a6f108adc/libjsound.so"}, "raw": {"sha1": "0c0fc63b92d7b83c9960fa80d45c80553ea20254", "size": 8232, "url": "https://launcher.mojang.com/v1/objects/0c0fc63b92d7b83c9960fa80d45c80553ea20254/libjsound.so"}}, "executable": true, "type": "file"}, "lib/amd64/libjsoundalsa.so": {"downloads": {"lzma": {"sha1": "b06c51858a25ff776519495f1b9b3d9f604b089f", "size": 23097, "url": "https://launcher.mojang.com/v1/objects/b06c51858a25ff776519495f1b9b3d9f604b089f/libjsoundalsa.so"}, "raw": {"sha1": "281d37f0326d4a12dc7ea316ead09c198ff7bdf7", "size": 83256, "url": "https://launcher.mojang.com/v1/objects/281d37f0326d4a12dc7ea316ead09c198ff7bdf7/libjsoundalsa.so"}}, "executable": true, "type": "file"}, "lib/amd64/liblcms.so": {"downloads": {"lzma": {"sha1": "7a239baba2086cae49114b382b74b971da02f08e", "size": 176175, "url": "https://launcher.mojang.com/v1/objects/7a239baba2086cae49114b382b74b971da02f08e/liblcms.so"}, "raw": {"sha1": "c8895cc3c3d023d9e059225969ab67954772c0a1", "size": 526872, "url": "https://launcher.mojang.com/v1/objects/c8895cc3c3d023d9e059225969ab67954772c0a1/liblcms.so"}}, "executable": true, "type": "file"}, "lib/amd64/libmanagement.so": {"downloads": {"lzma": {"sha1": "aed3fdbcefd1716abfc6a306687c8b741cbb318e", "size": 12838, "url": "https://launcher.mojang.com/v1/objects/aed3fdbcefd1716abfc6a306687c8b741cbb318e/libmanagement.so"}, "raw": {"sha1": "eba35f61e0d50e30874b7c7b335edf2d52662423", "size": 51808, "url": "https://launcher.mojang.com/v1/objects/eba35f61e0d50e30874b7c7b335edf2d52662423/libmanagement.so"}}, "executable": true, "type": "file"}, "lib/amd64/libmlib_image.so": {"downloads": {"lzma": {"sha1": "1bb181f079492d55c7a458e96488cd17fe0a7b86", "size": 310272, "url": "https://launcher.mojang.com/v1/objects/1bb181f079492d55c7a458e96488cd17fe0a7b86/libmlib_image.so"}, "raw": {"sha1": "c973c450d33873675945d4694be484e3427f58f1", "size": 1048136, "url": "https://launcher.mojang.com/v1/objects/c973c450d33873675945d4694be484e3427f58f1/libmlib_image.so"}}, "executable": true, "type": "file"}, "lib/amd64/libnet.so": {"downloads": {"lzma": {"sha1": "9dd79703b6deb86e0321afe01c6ac508263c8312", "size": 38123, "url": "https://launcher.mojang.com/v1/objects/9dd79703b6deb86e0321afe01c6ac508263c8312/libnet.so"}, "raw": {"sha1": "b3a17b7d53fcdf1e689e1ec29ce851eee6022ead", "size": 116920, "url": "https://launcher.mojang.com/v1/objects/b3a17b7d53fcdf1e689e1ec29ce851eee6022ead/libnet.so"}}, "executable": true, "type": "file"}, "lib/amd64/libnio.so": {"downloads": {"lzma": {"sha1": "5697c89d5d5d9b74f2e1555fcbba79dd4049e287", "size": 24445, "url": "https://launcher.mojang.com/v1/objects/5697c89d5d5d9b74f2e1555fcbba79dd4049e287/libnio.so"}, "raw": {"sha1": "573bf8f64dbcc397f8abd3e1da28f90ab0679f5b", "size": 93872, "url": "https://launcher.mojang.com/v1/objects/573bf8f64dbcc397f8abd3e1da28f90ab0679f5b/libnio.so"}}, "executable": true, "type": "file"}, "lib/amd64/libnpjp2.so": {"downloads": {"lzma": {"sha1": "6fe53b5951ff740e7f2ef7ffe5975af26da06718", "size": 57892, "url": "https://launcher.mojang.com/v1/objects/6fe53b5951ff740e7f2ef7ffe5975af26da06718/libnpjp2.so"}, "raw": {"sha1": "2bb13c53a4280379253475e51216b97eed1d4ce3", "size": 216592, "url": "https://launcher.mojang.com/v1/objects/2bb13c53a4280379253475e51216b97eed1d4ce3/libnpjp2.so"}}, "executable": true, "type": "file"}, "lib/amd64/libnpt.so": {"downloads": {"lzma": {"sha1": "1b170b09a32b1b8b6624fa5d1f94ec60b2bf3876", "size": 5070, "url": "https://launcher.mojang.com/v1/objects/1b170b09a32b1b8b6624fa5d1f94ec60b2bf3876/libnpt.so"}, "raw": {"sha1": "6b1ff6b9b4624f3cc7801f221c82b8046fb76364", "size": 17504, "url": "https://launcher.mojang.com/v1/objects/6b1ff6b9b4624f3cc7801f221c82b8046fb76364/libnpt.so"}}, "executable": true, "type": "file"}, "lib/amd64/libprism_common.so": {"downloads": {"lzma": {"sha1": "f4aca04c90bc7505851c074a08b2c31cae1f94fa", "size": 23315, "url": "https://launcher.mojang.com/v1/objects/f4aca04c90bc7505851c074a08b2c31cae1f94fa/libprism_common.so"}, "raw": {"sha1": "b00866b6ed8646a29a334d46e297267552f27de8", "size": 59008, "url": "https://launcher.mojang.com/v1/objects/b00866b6ed8646a29a334d46e297267552f27de8/libprism_common.so"}}, "executable": true, "type": "file"}, "lib/amd64/libprism_es2.so": {"downloads": {"lzma": {"sha1": "7ff4173c338c7a6f370f231670055737e032da3e", "size": 18416, "url": "https://launcher.mojang.com/v1/objects/7ff4173c338c7a6f370f231670055737e032da3e/libprism_es2.so"}, "raw": {"sha1": "1390a1dc14345e5a948148e59195d62f3a83863f", "size": 63808, "url": "https://launcher.mojang.com/v1/objects/1390a1dc14345e5a948148e59195d62f3a83863f/libprism_es2.so"}}, "executable": true, "type": "file"}, "lib/amd64/libprism_sw.so": {"downloads": {"lzma": {"sha1": "6728e8bf7b214067d715be6fe0325910d63c2468", "size": 29457, "url": "https://launcher.mojang.com/v1/objects/6728e8bf7b214067d715be6fe0325910d63c2468/libprism_sw.so"}, "raw": {"sha1": "7a6c34cb2bbcde411778d1b3f8677c39e32c3ac4", "size": 71608, "url": "https://launcher.mojang.com/v1/objects/7a6c34cb2bbcde411778d1b3f8677c39e32c3ac4/libprism_sw.so"}}, "executable": true, "type": "file"}, "lib/amd64/libresource.so": {"downloads": {"lzma": {"sha1": "1e35e63f1e74915fba620f1febf420b919d49bc5", "size": 2633, "url": "https://launcher.mojang.com/v1/objects/1e35e63f1e74915fba620f1febf420b919d49bc5/libresource.so"}, "raw": {"sha1": "57490353ad0d83ab1930355213dea269795434fe", "size": 13456, "url": "https://launcher.mojang.com/v1/objects/57490353ad0d83ab1930355213dea269795434fe/libresource.so"}}, "executable": true, "type": "file"}, "lib/amd64/libsctp.so": {"downloads": {"lzma": {"sha1": "4340132ed250d7849a016e071be773eaedd33aa8", "size": 9332, "url": "https://launcher.mojang.com/v1/objects/4340132ed250d7849a016e071be773eaedd33aa8/libsctp.so"}, "raw": {"sha1": "4a80e743750f127c0d5a359f5cd60b97e7ee12ae", "size": 29552, "url": "https://launcher.mojang.com/v1/objects/4a80e743750f127c0d5a359f5cd60b97e7ee12ae/libsctp.so"}}, "executable": true, "type": "file"}, "lib/amd64/libsplashscreen.so": {"downloads": {"lzma": {"sha1": "b226c8dbd73a548fc2b042ee6db6cc80e727597c", "size": 190305, "url": "https://launcher.mojang.com/v1/objects/b226c8dbd73a548fc2b042ee6db6cc80e727597c/libsplashscreen.so"}, "raw": {"sha1": "87d6a491f5ba7e6c4d972264a0c9063afea567a2", "size": 441376, "url": "https://launcher.mojang.com/v1/objects/87d6a491f5ba7e6c4d972264a0c9063afea567a2/libsplashscreen.so"}}, "executable": true, "type": "file"}, "lib/amd64/libsunec.so": {"downloads": {"lzma": {"sha1": "6ebba98fab1e3d872d1363235b76497f6f9babdc", "size": 88829, "url": "https://launcher.mojang.com/v1/objects/6ebba98fab1e3d872d1363235b76497f6f9babdc/libsunec.so"}, "raw": {"sha1": "3b262a0a530f6e4e539aed2cd27b4de1d0ed8859", "size": 283368, "url": "https://launcher.mojang.com/v1/objects/3b262a0a530f6e4e539aed2cd27b4de1d0ed8859/libsunec.so"}}, "executable": true, "type": "file"}, "lib/amd64/libt2k.so": {"downloads": {"lzma": {"sha1": "602cb812ef0b350ccf56ce209a260ddbe3743d92", "size": 190720, "url": "https://launcher.mojang.com/v1/objects/602cb812ef0b350ccf56ce209a260ddbe3743d92/libt2k.so"}, "raw": {"sha1": "b072c56df997f61e15e6b5a43b8907b0d25c2043", "size": 504840, "url": "https://launcher.mojang.com/v1/objects/b072c56df997f61e15e6b5a43b8907b0d25c2043/libt2k.so"}}, "executable": true, "type": "file"}, "lib/amd64/libunpack.so": {"downloads": {"lzma": {"sha1": "7107b615e941074f0b14c31c88fb67200aacd37f", "size": 70308, "url": "https://launcher.mojang.com/v1/objects/7107b615e941074f0b14c31c88fb67200aacd37f/libunpack.so"}, "raw": {"sha1": "b05ff862ed87928ed91e80e5604673c5ea710a53", "size": 197712, "url": "https://launcher.mojang.com/v1/objects/b05ff862ed87928ed91e80e5604673c5ea710a53/libunpack.so"}}, "executable": true, "type": "file"}, "lib/amd64/libverify.so": {"downloads": {"lzma": {"sha1": "ecd98efb8c7da441a8c3580e8f5598f3cb4165b1", "size": 21885, "url": "https://launcher.mojang.com/v1/objects/ecd98efb8c7da441a8c3580e8f5598f3cb4165b1/libverify.so"}, "raw": {"sha1": "e2c8d92531c45ab9be69ffb72c87fa12e9e59827", "size": 66112, "url": "https://launcher.mojang.com/v1/objects/e2c8d92531c45ab9be69ffb72c87fa12e9e59827/libverify.so"}}, "executable": true, "type": "file"}, "lib/amd64/libzip.so": {"downloads": {"lzma": {"sha1": "7c562342e3f7b138dc978495447e3e6a96c2cf45", "size": 54876, "url": "https://launcher.mojang.com/v1/objects/7c562342e3f7b138dc978495447e3e6a96c2cf45/libzip.so"}, "raw": {"sha1": "5f4bf35a5c3e8f8c650e891d1031589b8ab6d77f", "size": 127016, "url": "https://launcher.mojang.com/v1/objects/5f4bf35a5c3e8f8c650e891d1031589b8ab6d77f/libzip.so"}}, "executable": true, "type": "file"}, "lib/amd64/server": {"type": "directory"}, "lib/amd64/server/Xusage.txt": {"downloads": {"lzma": {"sha1": "acb2da24a4c765887df83985e4c26d6be302a0a3", "size": 629, "url": "https://launcher.mojang.com/v1/objects/acb2da24a4c765887df83985e4c26d6be302a0a3/Xusage.txt"}, "raw": {"sha1": "6983727eafe140f9dd793c78aa6f3e007438243a", "size": 1423, "url": "https://launcher.mojang.com/v1/objects/6983727eafe140f9dd793c78aa6f3e007438243a/Xusage.txt"}}, "executable": false, "type": "file"}, "lib/amd64/server/libjsig.so": {"target": "../libjsig.so", "type": "link"}, "lib/amd64/server/libjvm.so": {"downloads": {"lzma": {"sha1": "d5c6f3338aaa6712f79d680ac8c3e31beebaa886", "size": 4154311, "url": "https://launcher.mojang.com/v1/objects/d5c6f3338aaa6712f79d680ac8c3e31beebaa886/libjvm.so"}, "raw": {"sha1": "23a98e1eb505cc3fb91bc0cb2adb71ab9270e9ca", "size": 17045016, "url": "https://launcher.mojang.com/v1/objects/23a98e1eb505cc3fb91bc0cb2adb71ab9270e9ca/libjvm.so"}}, "executable": true, "type": "file"}, "lib/applet": {"type": "directory"}, "lib/calendars.properties": {"downloads": {"lzma": {"sha1": "4a757c23f2942bd802a4f80235131146d9267750", "size": 558, "url": "https://launcher.mojang.com/v1/objects/4a757c23f2942bd802a4f80235131146d9267750/calendars.properties"}, "raw": {"sha1": "42ebb0988124433b8f2a6e5d9a74ed41240bcfc6", "size": 1378, "url": "https://launcher.mojang.com/v1/objects/42ebb0988124433b8f2a6e5d9a74ed41240bcfc6/calendars.properties"}}, "executable": false, "type": "file"}, "lib/charsets.jar": {"downloads": {"lzma": {"sha1": "2bf44143b2ad9d7d55045a4de4a562330c194dc0", "size": 412367, "url": "https://launcher.mojang.com/v1/objects/2bf44143b2ad9d7d55045a4de4a562330c194dc0/charsets.jar"}, "raw": {"sha1": "d73ab9f8de255a7e112ddd13622bf7f6b18c8447", "size": 3135615, "url": "https://launcher.mojang.com/v1/objects/d73ab9f8de255a7e112ddd13622bf7f6b18c8447/charsets.jar"}}, "executable": false, "type": "file"}, "lib/classlist": {"downloads": {"lzma": {"sha1": "14e7c73d21b8513b0aff8d86e5cb34c52021fbca", "size": 15024, "url": "https://launcher.mojang.com/v1/objects/14e7c73d21b8513b0aff8d86e5cb34c52021fbca/classlist"}, "raw": {"sha1": "9c0404b63c87e2fed35e3a6cd137d6cf876c42bd", "size": 84311, "url": "https://launcher.mojang.com/v1/objects/9c0404b63c87e2fed35e3a6cd137d6cf876c42bd/classlist"}}, "executable": false, "type": "file"}, "lib/cmm": {"type": "directory"}, "lib/cmm/CIEXYZ.pf": {"downloads": {"lzma": {"sha1": "fcc5ca2fd3f45cac3434b480fa3ce00007e96529", "size": 8964, "url": "https://launcher.mojang.com/v1/objects/fcc5ca2fd3f45cac3434b480fa3ce00007e96529/CIEXYZ.pf"}, "raw": {"sha1": "b7779924c70554647b87c2a86159ca7781e929f8", "size": 51236, "url": "https://launcher.mojang.com/v1/objects/b7779924c70554647b87c2a86159ca7781e929f8/CIEXYZ.pf"}}, "executable": false, "type": "file"}, "lib/cmm/GRAY.pf": {"downloads": {"lzma": {"sha1": "5388ccfe67d3131d6d02143d8e8895003ab14ff6", "size": 299, "url": "https://launcher.mojang.com/v1/objects/5388ccfe67d3131d6d02143d8e8895003ab14ff6/GRAY.pf"}, "raw": {"sha1": "27f93961d66b8230d0cdb8b166bc8b4153d5bc2d", "size": 632, "url": "https://launcher.mojang.com/v1/objects/27f93961d66b8230d0cdb8b166bc8b4153d5bc2d/GRAY.pf"}}, "executable": false, "type": "file"}, "lib/cmm/LINEAR_RGB.pf": {"downloads": {"lzma": {"sha1": "2bd90f09c8deb64b1729d6b8173c78f9e9cab27b", "size": 678, "url": "https://launcher.mojang.com/v1/objects/2bd90f09c8deb64b1729d6b8173c78f9e9cab27b/LINEAR_RGB.pf"}, "raw": {"sha1": "7913274c2f73bafcf888f09ff60990b100214ede", "size": 1044, "url": "https://launcher.mojang.com/v1/objects/7913274c2f73bafcf888f09ff60990b100214ede/LINEAR_RGB.pf"}}, "executable": false, "type": "file"}, "lib/cmm/PYCC.pf": {"downloads": {"lzma": {"sha1": "dbb2197ecff3fcdd142e9006490c8cb5c8d19af8", "size": 171521, "url": "https://launcher.mojang.com/v1/objects/dbb2197ecff3fcdd142e9006490c8cb5c8d19af8/PYCC.pf"}, "raw": {"sha1": "4f7eed05b8f0eea7bcdc8f8f7aaeb1925ce7b144", "size": 274474, "url": "https://launcher.mojang.com/v1/objects/4f7eed05b8f0eea7bcdc8f8f7aaeb1925ce7b144/PYCC.pf"}}, "executable": false, "type": "file"}, "lib/cmm/sRGB.pf": {"downloads": {"raw": {"sha1": "9eaea0911d89d63e39e95f2e2116eaec7e0bb91e", "size": 3144, "url": "https://launcher.mojang.com/v1/objects/9eaea0911d89d63e39e95f2e2116eaec7e0bb91e/sRGB.pf"}}, "executable": false, "type": "file"}, "lib/content-types.properties": {"downloads": {"lzma": {"sha1": "43a23d9a6c637c128b14cfa3feced93cbcf85b1a", "size": 1617, "url": "https://launcher.mojang.com/v1/objects/43a23d9a6c637c128b14cfa3feced93cbcf85b1a/content-types.properties"}, "raw": {"sha1": "b21698017c4a2866b5fabe59681b7592e72c83b1", "size": 5916, "url": "https://launcher.mojang.com/v1/objects/b21698017c4a2866b5fabe59681b7592e72c83b1/content-types.properties"}}, "executable": false, "type": "file"}, "lib/currency.data": {"downloads": {"lzma": {"sha1": "451b3f166ea34ef2aefbb01606ea5adcc0d65b42", "size": 1184, "url": "https://launcher.mojang.com/v1/objects/451b3f166ea34ef2aefbb01606ea5adcc0d65b42/currency.data"}, "raw": {"sha1": "bf524381a7a9b9d5bbab48069c583d2936e367a1", "size": 4134, "url": "https://launcher.mojang.com/v1/objects/bf524381a7a9b9d5bbab48069c583d2936e367a1/currency.data"}}, "executable": false, "type": "file"}, "lib/deploy": {"type": "directory"}, "lib/deploy.jar": {"downloads": {"raw": {"sha1": "fbe1de8fcd9a3d482c59414dce9311e4194766c9", "size": 2255881, "url": "https://launcher.mojang.com/v1/objects/fbe1de8fcd9a3d482c59414dce9311e4194766c9/deploy.jar"}}, "executable": false, "type": "file"}, "lib/deploy/MixedCodeMainDialog.ui": {"downloads": {"lzma": {"sha1": "7d812964343d1e978442f5c847c709784fc18fc0", "size": 683, "url": "https://launcher.mojang.com/v1/objects/7d812964343d1e978442f5c847c709784fc18fc0/MixedCodeMainDialog.ui"}, "raw": {"sha1": "c9b1af1c229e54b2d8a3d642d4f0bb31dc15be79", "size": 4507, "url": "https://launcher.mojang.com/v1/objects/c9b1af1c229e54b2d8a3d642d4f0bb31dc15be79/MixedCodeMainDialog.ui"}}, "executable": false, "type": "file"}, "lib/deploy/MixedCodeMainDialogJs.ui": {"downloads": {"lzma": {"sha1": "54fb58dbcc59e35e0ae896d0e266ae0c5bcf85c2", "size": 792, "url": "https://launcher.mojang.com/v1/objects/54fb58dbcc59e35e0ae896d0e266ae0c5bcf85c2/MixedCodeMainDialogJs.ui"}, "raw": {"sha1": "ad6337fb6d46750e14c12b439a5856f4b6864d0d", "size": 6110, "url": "https://launcher.mojang.com/v1/objects/ad6337fb6d46750e14c12b439a5856f4b6864d0d/MixedCodeMainDialogJs.ui"}}, "executable": false, "type": "file"}, "lib/deploy/cautionshield.icns": {"downloads": {"lzma": {"sha1": "7cea751dc168605054ec38ce8bfa71812be405c1", "size": 2333, "url": "https://launcher.mojang.com/v1/objects/7cea751dc168605054ec38ce8bfa71812be405c1/cautionshield.icns"}, "raw": {"sha1": "1de7ed5d5fc75aa1bcede088c655bee3bde64c38", "size": 3588, "url": "https://launcher.mojang.com/v1/objects/1de7ed5d5fc75aa1bcede088c655bee3bde64c38/cautionshield.icns"}}, "executable": false, "type": "file"}, "lib/deploy/ffjcext.zip": {"downloads": {"lzma": {"sha1": "80bcb9b3794f69d87dba93e90230f288e651e798", "size": 1809, "url": "https://launcher.mojang.com/v1/objects/80bcb9b3794f69d87dba93e90230f288e651e798/ffjcext.zip"}, "raw": {"sha1": "76d051ca7d3666ff25ea8eb9957a05574a45287f", "size": 13454, "url": "https://launcher.mojang.com/v1/objects/76d051ca7d3666ff25ea8eb9957a05574a45287f/ffjcext.zip"}}, "executable": false, "type": "file"}, "lib/deploy/java-icon.ico": {"downloads": {"lzma": {"sha1": "2a24f0207d7ab5976a8b0d92b4b381d49e895c9d", "size": 8468, "url": "https://launcher.mojang.com/v1/objects/2a24f0207d7ab5976a8b0d92b4b381d49e895c9d/java-icon.ico"}, "raw": {"sha1": "2997ceb26ff49a7d7c5e7a2405b5fb50b62c7914", "size": 29926, "url": "https://launcher.mojang.com/v1/objects/2997ceb26ff49a7d7c5e7a2405b5fb50b62c7914/java-icon.ico"}}, "executable": false, "type": "file"}, "lib/deploy/messages.properties": {"downloads": {"lzma": {"sha1": "c1e16f80dc0b1f1a591cecf3cbab4ba5e47492f4", "size": 1225, "url": "https://launcher.mojang.com/v1/objects/c1e16f80dc0b1f1a591cecf3cbab4ba5e47492f4/messages.properties"}, "raw": {"sha1": "dc52841c708e3c1eb2a044088a43396d1291bb5e", "size": 2860, "url": "https://launcher.mojang.com/v1/objects/dc52841c708e3c1eb2a044088a43396d1291bb5e/messages.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_de.properties": {"downloads": {"lzma": {"sha1": "42b42e6e1d2cb2d781f2226bde612ce519b29bc8", "size": 1394, "url": "https://launcher.mojang.com/v1/objects/42b42e6e1d2cb2d781f2226bde612ce519b29bc8/messages_de.properties"}, "raw": {"sha1": "d989fe1b8f7904888d5102294ebefd28d932ecdb", "size": 3306, "url": "https://launcher.mojang.com/v1/objects/d989fe1b8f7904888d5102294ebefd28d932ecdb/messages_de.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_es.properties": {"downloads": {"lzma": {"sha1": "c4a653e9802ca982e892b45d88c1e259c09e8c8e", "size": 1404, "url": "https://launcher.mojang.com/v1/objects/c4a653e9802ca982e892b45d88c1e259c09e8c8e/messages_es.properties"}, "raw": {"sha1": "1b0334b79db481c3a59be6915d5118d760c97baa", "size": 3600, "url": "https://launcher.mojang.com/v1/objects/1b0334b79db481c3a59be6915d5118d760c97baa/messages_es.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_fr.properties": {"downloads": {"lzma": {"sha1": "2d8dee07e3f5aab7318a22e169810b216ac44f97", "size": 1401, "url": "https://launcher.mojang.com/v1/objects/2d8dee07e3f5aab7318a22e169810b216ac44f97/messages_fr.properties"}, "raw": {"sha1": "69bd2d03c2064f8679de5b4e430ea61b567c69c5", "size": 3409, "url": "https://launcher.mojang.com/v1/objects/69bd2d03c2064f8679de5b4e430ea61b567c69c5/messages_fr.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_it.properties": {"downloads": {"lzma": {"sha1": "7c28cdd8d9e34355ba0fc03004c4f64749cae57e", "size": 1375, "url": "https://launcher.mojang.com/v1/objects/7c28cdd8d9e34355ba0fc03004c4f64749cae57e/messages_it.properties"}, "raw": {"sha1": "dbe49949308f28540a42ae6cd2ad58afbf615592", "size": 3223, "url": "https://launcher.mojang.com/v1/objects/dbe49949308f28540a42ae6cd2ad58afbf615592/messages_it.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_ja.properties": {"downloads": {"lzma": {"sha1": "9a6a4c086e48b9e615b72b6bbebb3c724d178ff4", "size": 1680, "url": "https://launcher.mojang.com/v1/objects/9a6a4c086e48b9e615b72b6bbebb3c724d178ff4/messages_ja.properties"}, "raw": {"sha1": "751170a7cdefcb1226604ac3f8196e06a04fd7ac", "size": 6349, "url": "https://launcher.mojang.com/v1/objects/751170a7cdefcb1226604ac3f8196e06a04fd7ac/messages_ja.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_ko.properties": {"downloads": {"lzma": {"sha1": "0c57c2ebfa0830f816657a384898487fc492efac", "size": 1645, "url": "https://launcher.mojang.com/v1/objects/0c57c2ebfa0830f816657a384898487fc492efac/messages_ko.properties"}, "raw": {"sha1": "bf9e055b5ab138ad6d49769e2b7630b7938848d6", "size": 5712, "url": "https://launcher.mojang.com/v1/objects/bf9e055b5ab138ad6d49769e2b7630b7938848d6/messages_ko.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_pt_BR.properties": {"downloads": {"lzma": {"sha1": "f8364dba0aa0a7e445a1a8d0e7ad66b996f70063", "size": 1388, "url": "https://launcher.mojang.com/v1/objects/f8364dba0aa0a7e445a1a8d0e7ad66b996f70063/messages_pt_BR.properties"}, "raw": {"sha1": "24e4951743521ab9a11381c77bd0cdb1ed30f5b5", "size": 3285, "url": "https://launcher.mojang.com/v1/objects/24e4951743521ab9a11381c77bd0cdb1ed30f5b5/messages_pt_BR.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_sv.properties": {"downloads": {"lzma": {"sha1": "65e5897d552258141aacf02f087c7c9c33ad0727", "size": 1355, "url": "https://launcher.mojang.com/v1/objects/65e5897d552258141aacf02f087c7c9c33ad0727/messages_sv.properties"}, "raw": {"sha1": "bb5a4aa0ba499f6b1916a83e3c7922a4583b4adb", "size": 3384, "url": "https://launcher.mojang.com/v1/objects/bb5a4aa0ba499f6b1916a83e3c7922a4583b4adb/messages_sv.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_zh_CN.properties": {"downloads": {"lzma": {"sha1": "de7d39a6e6748e9f47e842c9da90f515921c222c", "size": 1506, "url": "https://launcher.mojang.com/v1/objects/de7d39a6e6748e9f47e842c9da90f515921c222c/messages_zh_CN.properties"}, "raw": {"sha1": "1c2b96673dddd3596890ef4fc22017d484a1f652", "size": 4072, "url": "https://launcher.mojang.com/v1/objects/1c2b96673dddd3596890ef4fc22017d484a1f652/messages_zh_CN.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_zh_HK.properties": {"downloads": {"lzma": {"sha1": "e8d0e3a63caa2535a4f361033941f34dcc170a7e", "size": 1529, "url": "https://launcher.mojang.com/v1/objects/e8d0e3a63caa2535a4f361033941f34dcc170a7e/messages_zh_TW.properties"}, "raw": {"sha1": "37a57aad121c14c25e149206179728fa62203bf0", "size": 3752, "url": "https://launcher.mojang.com/v1/objects/37a57aad121c14c25e149206179728fa62203bf0/messages_zh_TW.properties"}}, "executable": false, "type": "file"}, "lib/deploy/messages_zh_TW.properties": {"downloads": {"lzma": {"sha1": "e8d0e3a63caa2535a4f361033941f34dcc170a7e", "size": 1529, "url": "https://launcher.mojang.com/v1/objects/e8d0e3a63caa2535a4f361033941f34dcc170a7e/messages_zh_TW.properties"}, "raw": {"sha1": "37a57aad121c14c25e149206179728fa62203bf0", "size": 3752, "url": "https://launcher.mojang.com/v1/objects/37a57aad121c14c25e149206179728fa62203bf0/messages_zh_TW.properties"}}, "executable": false, "type": "file"}, "lib/deploy/mixcode_s.png": {"downloads": {"raw": {"sha1": "4604e9f265eec97bccd0151c3a81afa9e69499e5", "size": 3115, "url": "https://launcher.mojang.com/v1/objects/4604e9f265eec97bccd0151c3a81afa9e69499e5/mixcode_s.png"}}, "executable": false, "type": "file"}, "lib/deploy/splash.gif": {"downloads": {"raw": {"sha1": "20e7aec75f6d036d504277542e507eb7dc24aae8", "size": 8590, "url": "https://launcher.mojang.com/v1/objects/20e7aec75f6d036d504277542e507eb7dc24aae8/splash.gif"}}, "executable": false, "type": "file"}, "lib/deploy/splash@2x.gif": {"downloads": {"raw": {"sha1": "0ae4a5bda2a6d628fac51462390b503c99509fdc", "size": 15276, "url": "https://launcher.mojang.com/v1/objects/0ae4a5bda2a6d628fac51462390b503c99509fdc/splash2x.gif"}}, "executable": false, "type": "file"}, "lib/deploy/splash_11-lic.gif": {"downloads": {"raw": {"sha1": "8def364e07f40142822df84b5bb4f50846cb5e4e", "size": 7805, "url": "https://launcher.mojang.com/v1/objects/8def364e07f40142822df84b5bb4f50846cb5e4e/splash_11-lic.gif"}}, "executable": false, "type": "file"}, "lib/deploy/splash_11@2x-lic.gif": {"downloads": {"raw": {"sha1": "d2bff9bbf7920ca743b81a0ee23b0719b4d057ca", "size": 12250, "url": "https://launcher.mojang.com/v1/objects/d2bff9bbf7920ca743b81a0ee23b0719b4d057ca/splash_11%402x-lic.gif"}}, "executable": false, "type": "file"}, "lib/desktop": {"type": "directory"}, "lib/desktop/applications": {"type": "directory"}, "lib/desktop/applications/sun-java.desktop": {"downloads": {"lzma": {"sha1": "109d1cdf165f38da92da70b403ca940192a7a9a8", "size": 536, "url": "https://launcher.mojang.com/v1/objects/109d1cdf165f38da92da70b403ca940192a7a9a8/sun-java.desktop"}, "raw": {"sha1": "d346dfe90505603ce5aff5a3c6c2e4a23d5bd990", "size": 777, "url": "https://launcher.mojang.com/v1/objects/d346dfe90505603ce5aff5a3c6c2e4a23d5bd990/sun-java.desktop"}}, "executable": false, "type": "file"}, "lib/desktop/applications/sun-javaws.desktop": {"downloads": {"lzma": {"sha1": "5e1815e7f83515881e6998584dc6bb02c5bef09a", "size": 451, "url": "https://launcher.mojang.com/v1/objects/5e1815e7f83515881e6998584dc6bb02c5bef09a/sun-javaws.desktop"}, "raw": {"sha1": "50ce8e519b836e0f53d58ce1a359d98b6cafdda6", "size": 619, "url": "https://launcher.mojang.com/v1/objects/50ce8e519b836e0f53d58ce1a359d98b6cafdda6/sun-javaws.desktop"}}, "executable": false, "type": "file"}, "lib/desktop/applications/sun_java.desktop": {"downloads": {"lzma": {"sha1": "49ab0ccb54c3be68281d05055bc56a88b1281d3c", "size": 447, "url": "https://launcher.mojang.com/v1/objects/49ab0ccb54c3be68281d05055bc56a88b1281d3c/sun_java.desktop"}, "raw": {"sha1": "79120ee8160ad6f3c9b90c2641fb7edf3af96b5d", "size": 624, "url": "https://launcher.mojang.com/v1/objects/79120ee8160ad6f3c9b90c2641fb7edf3af96b5d/sun_java.desktop"}}, "executable": false, "type": "file"}, "lib/desktop/icons": {"type": "directory"}, "lib/desktop/icons/HighContrast": {"type": "directory"}, "lib/desktop/icons/HighContrast/16x16": {"type": "directory"}, "lib/desktop/icons/HighContrast/16x16/apps": {"type": "directory"}, "lib/desktop/icons/HighContrast/16x16/apps/sun-java.png": {"downloads": {"raw": {"sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "366e7a48e9e4fb92eaeabbcaeb4626122a66cecb", "size": 417, "url": "https://launcher.mojang.com/v1/objects/366e7a48e9e4fb92eaeabbcaeb4626122a66cecb/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/mimetypes": {"type": "directory"}, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/16x16/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "629c48907368ecf32d2395b6459c367f79d84689", "size": 464, "url": "https://launcher.mojang.com/v1/objects/629c48907368ecf32d2395b6459c367f79d84689/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48": {"type": "directory"}, "lib/desktop/icons/HighContrast/48x48/apps": {"type": "directory"}, "lib/desktop/icons/HighContrast/48x48/apps/sun-java.png": {"downloads": {"raw": {"sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "8373482d072684e09830dbdb97a76ea264c9f4e9", "size": 3451, "url": "https://launcher.mojang.com/v1/objects/8373482d072684e09830dbdb97a76ea264c9f4e9/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/mimetypes": {"type": "directory"}, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrast/48x48/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "56a4996519f8f3541eba7b7a7a69bcdcd8ed0410", "size": 2088, "url": "https://launcher.mojang.com/v1/objects/56a4996519f8f3541eba7b7a7a69bcdcd8ed0410/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/16x16": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/16x16/apps": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-java.png": {"downloads": {"raw": {"sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "bf0995acb94aa794e73c5b971282ff13ffe42793", "size": 402, "url": "https://launcher.mojang.com/v1/objects/bf0995acb94aa794e73c5b971282ff13ffe42793/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/16x16/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "1477eceda25e162fcda2e69ee3906091973d8344", "size": 473, "url": "https://launcher.mojang.com/v1/objects/1477eceda25e162fcda2e69ee3906091973d8344/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/48x48/apps": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-java.png": {"downloads": {"raw": {"sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "413da160dd9899b95f53d4cc11f5ee0550cc6585", "size": 3410, "url": "https://launcher.mojang.com/v1/objects/413da160dd9899b95f53d4cc11f5ee0550cc6585/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes": {"type": "directory"}, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/HighContrastInverse/48x48/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "d66e04dfa25c196bec2e201547325b79846ab674", "size": 2085, "url": "https://launcher.mojang.com/v1/objects/d66e04dfa25c196bec2e201547325b79846ab674/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast": {"type": "directory"}, "lib/desktop/icons/LowContrast/16x16": {"type": "directory"}, "lib/desktop/icons/LowContrast/16x16/apps": {"type": "directory"}, "lib/desktop/icons/LowContrast/16x16/apps/sun-java.png": {"downloads": {"raw": {"sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "f93b7cf0a6d27d664a7f09dab6933b2768536f52", "size": 519, "url": "https://launcher.mojang.com/v1/objects/f93b7cf0a6d27d664a7f09dab6933b2768536f52/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/mimetypes": {"type": "directory"}, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/16x16/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "0aa1605877280b88de1f1cc3e7e4bdbeed968a73", "size": 525, "url": "https://launcher.mojang.com/v1/objects/0aa1605877280b88de1f1cc3e7e4bdbeed968a73/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48": {"type": "directory"}, "lib/desktop/icons/LowContrast/48x48/apps": {"type": "directory"}, "lib/desktop/icons/LowContrast/48x48/apps/sun-java.png": {"downloads": {"raw": {"sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "1fcf4fd6da61873b5f21b39412da26509734b7cc", "size": 1507, "url": "https://launcher.mojang.com/v1/objects/1fcf4fd6da61873b5f21b39412da26509734b7cc/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/mimetypes": {"type": "directory"}, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/LowContrast/48x48/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "e36636b1c04dc283c18adf669b892d54b15d3ee6", "size": 1948, "url": "https://launcher.mojang.com/v1/objects/e36636b1c04dc283c18adf669b892d54b15d3ee6/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor": {"type": "directory"}, "lib/desktop/icons/hicolor/16x16": {"type": "directory"}, "lib/desktop/icons/hicolor/16x16/apps": {"type": "directory"}, "lib/desktop/icons/hicolor/16x16/apps/sun-java.png": {"downloads": {"raw": {"sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "e91d05bfe9b889bf8a227908b597cab4630da8f2", "size": 383, "url": "https://launcher.mojang.com/v1/objects/e91d05bfe9b889bf8a227908b597cab4630da8f2/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/mimetypes": {"type": "directory"}, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/16x16/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "d2f6abe8e498aeecb334fb43f63001d34dbf6ea5", "size": 783, "url": "https://launcher.mojang.com/v1/objects/d2f6abe8e498aeecb334fb43f63001d34dbf6ea5/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48": {"type": "directory"}, "lib/desktop/icons/hicolor/48x48/apps": {"type": "directory"}, "lib/desktop/icons/hicolor/48x48/apps/sun-java.png": {"downloads": {"raw": {"sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/apps/sun-javaws.png": {"downloads": {"raw": {"sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/apps/sun-jcontrol.png": {"downloads": {"raw": {"sha1": "6c90a38eaada9c32a678a282be18ec5b43a84264", "size": 1439, "url": "https://launcher.mojang.com/v1/objects/6c90a38eaada9c32a678a282be18ec5b43a84264/sun-javaws.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/mimetypes": {"type": "directory"}, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-application-x-java-archive.png": {"downloads": {"raw": {"sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-application-x-java-jnlp-file.png": {"downloads": {"raw": {"sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/icons/hicolor/48x48/mimetypes/gnome-mime-text-x-java.png": {"downloads": {"raw": {"sha1": "4d5e6e0c41d1076bc86f3ab157c88a41a5716997", "size": 3202, "url": "https://launcher.mojang.com/v1/objects/4d5e6e0c41d1076bc86f3ab157c88a41a5716997/gnome-mime-application-x-java-archive.png"}}, "executable": false, "type": "file"}, "lib/desktop/mime": {"type": "directory"}, "lib/desktop/mime/packages": {"type": "directory"}, "lib/desktop/mime/packages/x-java-archive.xml": {"downloads": {"lzma": {"sha1": "841230729f0a59de2a1071d155d96358232b2ba1", "size": 591, "url": "https://launcher.mojang.com/v1/objects/841230729f0a59de2a1071d155d96358232b2ba1/x-java-archive.xml"}, "raw": {"sha1": "b6297fd36efa799312961f95ebf0c85c920d5037", "size": 1822, "url": "https://launcher.mojang.com/v1/objects/b6297fd36efa799312961f95ebf0c85c920d5037/x-java-archive.xml"}}, "executable": false, "type": "file"}, "lib/desktop/mime/packages/x-java-jnlp-file.xml": {"downloads": {"lzma": {"sha1": "abf9acbe7c18027c4f036c4e42bb2cf1115525fa", "size": 302, "url": "https://launcher.mojang.com/v1/objects/abf9acbe7c18027c4f036c4e42bb2cf1115525fa/x-java-jnlp-file.xml"}, "raw": {"sha1": "72f03da83ddb76c9105f619fcfa4dbdad70e6b30", "size": 412, "url": "https://launcher.mojang.com/v1/objects/72f03da83ddb76c9105f619fcfa4dbdad70e6b30/x-java-jnlp-file.xml"}}, "executable": false, "type": "file"}, "lib/ext": {"type": "directory"}, "lib/ext/cldrdata.jar": {"downloads": {"raw": {"sha1": "6cacc961942d3f02a88907aa8f2eaae8e20c95a0", "size": 3860502, "url": "https://launcher.mojang.com/v1/objects/6cacc961942d3f02a88907aa8f2eaae8e20c95a0/cldrdata.jar"}}, "executable": false, "type": "file"}, "lib/ext/dnsns.jar": {"downloads": {"raw": {"sha1": "93bebdd7514e53ae31d60c5daba673878c8822ec", "size": 8286, "url": "https://launcher.mojang.com/v1/objects/93bebdd7514e53ae31d60c5daba673878c8822ec/dnsns.jar"}}, "executable": false, "type": "file"}, "lib/ext/jaccess.jar": {"downloads": {"raw": {"sha1": "2f54879df7c29ec67c40d40cfc95c0d4a968bea1", "size": 44516, "url": "https://launcher.mojang.com/v1/objects/2f54879df7c29ec67c40d40cfc95c0d4a968bea1/jaccess.jar"}}, "executable": false, "type": "file"}, "lib/ext/jfxrt.jar": {"downloads": {"lzma": {"sha1": "a6c5b6a782ba360ada6651f5322dcab88c75711c", "size": 3374270, "url": "https://launcher.mojang.com/v1/objects/a6c5b6a782ba360ada6651f5322dcab88c75711c/jfxrt.jar"}, "raw": {"sha1": "1ad7a876f045399c23ee4ab7dee380a04ca2ac08", "size": 18508094, "url": "https://launcher.mojang.com/v1/objects/1ad7a876f045399c23ee4ab7dee380a04ca2ac08/jfxrt.jar"}}, "executable": false, "type": "file"}, "lib/ext/localedata.jar": {"downloads": {"raw": {"sha1": "0cc9f550d4e410b5aa29dbfd2c1b5c99391c7f70", "size": 1178926, "url": "https://launcher.mojang.com/v1/objects/0cc9f550d4e410b5aa29dbfd2c1b5c99391c7f70/localedata.jar"}}, "executable": false, "type": "file"}, "lib/ext/meta-index": {"downloads": {"lzma": {"sha1": "1359457529f42bacf495afcb68149ae036442dd9", "size": 594, "url": "https://launcher.mojang.com/v1/objects/1359457529f42bacf495afcb68149ae036442dd9/meta-index"}, "raw": {"sha1": "334649c6e2d5d7248211f30855e97cfcb4558851", "size": 1269, "url": "https://launcher.mojang.com/v1/objects/334649c6e2d5d7248211f30855e97cfcb4558851/meta-index"}}, "executable": false, "type": "file"}, "lib/ext/nashorn.jar": {"downloads": {"raw": {"sha1": "dec5dd17a0f52ae79dfbfb38840bffb8b7a679a5", "size": 2023869, "url": "https://launcher.mojang.com/v1/objects/dec5dd17a0f52ae79dfbfb38840bffb8b7a679a5/nashorn.jar"}}, "executable": false, "type": "file"}, "lib/ext/sunec.jar": {"downloads": {"raw": {"sha1": "bf1c817820341a246f7130fe046e8310b03d04f6", "size": 41672, "url": "https://launcher.mojang.com/v1/objects/bf1c817820341a246f7130fe046e8310b03d04f6/sunec.jar"}}, "executable": false, "type": "file"}, "lib/ext/sunjce_provider.jar": {"downloads": {"raw": {"sha1": "bb3494e4b5cb3c3e60da767207731f18b267cb34", "size": 279326, "url": "https://launcher.mojang.com/v1/objects/bb3494e4b5cb3c3e60da767207731f18b267cb34/sunjce_provider.jar"}}, "executable": false, "type": "file"}, "lib/ext/sunpkcs11.jar": {"downloads": {"raw": {"sha1": "5bb1dedc3344cd3bb86828d4aa8ca82f4a606ed4", "size": 250218, "url": "https://launcher.mojang.com/v1/objects/5bb1dedc3344cd3bb86828d4aa8ca82f4a606ed4/sunpkcs11.jar"}}, "executable": false, "type": "file"}, "lib/ext/zipfs.jar": {"downloads": {"raw": {"sha1": "37b338f0e8e60d6396f51275130e8110816d7b30", "size": 68964, "url": "https://launcher.mojang.com/v1/objects/37b338f0e8e60d6396f51275130e8110816d7b30/zipfs.jar"}}, "executable": false, "type": "file"}, "lib/flavormap.properties": {"downloads": {"lzma": {"sha1": "2d5ef19ee77ccfc95c9413eea155cde59a48fadd", "size": 1541, "url": "https://launcher.mojang.com/v1/objects/2d5ef19ee77ccfc95c9413eea155cde59a48fadd/flavormap.properties"}, "raw": {"sha1": "4e66e8fe12d7f8b3b0c4e1a1915f329bb1fbf6d2", "size": 3901, "url": "https://launcher.mojang.com/v1/objects/4e66e8fe12d7f8b3b0c4e1a1915f329bb1fbf6d2/flavormap.properties"}}, "executable": false, "type": "file"}, "lib/fontconfig.RedHat.5.bfc": {"downloads": {"lzma": {"sha1": "5197f6e387f16458b7408134e38b06f20f625e4c", "size": 795, "url": "https://launcher.mojang.com/v1/objects/5197f6e387f16458b7408134e38b06f20f625e4c/fontconfig.RedHat.5.bfc"}, "raw": {"sha1": "fb806ada6e68f16a9fe2b726a39d9ef5a835c0c2", "size": 4532, "url": "https://launcher.mojang.com/v1/objects/fb806ada6e68f16a9fe2b726a39d9ef5a835c0c2/fontconfig.RedHat.5.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.RedHat.5.properties.src": {"downloads": {"lzma": {"sha1": "3897ae198e96e5a687c9c9b218ff5df60c868e0d", "size": 1089, "url": "https://launcher.mojang.com/v1/objects/3897ae198e96e5a687c9c9b218ff5df60c868e0d/fontconfig.RedHat.5.properties.src"}, "raw": {"sha1": "c67d1a06cb37b66e69560c9f5e4be7cf08af0493", "size": 8841, "url": "https://launcher.mojang.com/v1/objects/c67d1a06cb37b66e69560c9f5e4be7cf08af0493/fontconfig.RedHat.5.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.RedHat.6.bfc": {"downloads": {"lzma": {"sha1": "ef2f5d1f8d620be9927db45d3a28bd75777245cb", "size": 818, "url": "https://launcher.mojang.com/v1/objects/ef2f5d1f8d620be9927db45d3a28bd75777245cb/fontconfig.RedHat.6.bfc"}, "raw": {"sha1": "9ba3b3e2c621c31d0ef1b2053c80f77419a19965", "size": 4250, "url": "https://launcher.mojang.com/v1/objects/9ba3b3e2c621c31d0ef1b2053c80f77419a19965/fontconfig.RedHat.6.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.RedHat.6.properties.src": {"downloads": {"lzma": {"sha1": "74f4148f9d7ec3d67bbd724834d478a72cfdb0db", "size": 1111, "url": "https://launcher.mojang.com/v1/objects/74f4148f9d7ec3d67bbd724834d478a72cfdb0db/fontconfig.RedHat.6.properties.src"}, "raw": {"sha1": "768e58d4d314621c38daf9fde6d67119f370acd9", "size": 8735, "url": "https://launcher.mojang.com/v1/objects/768e58d4d314621c38daf9fde6d67119f370acd9/fontconfig.RedHat.6.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.SuSE.10.bfc": {"downloads": {"lzma": {"sha1": "d8fe9b1d8d02368dcd452de93024c6f60670eb87", "size": 1083, "url": "https://launcher.mojang.com/v1/objects/d8fe9b1d8d02368dcd452de93024c6f60670eb87/fontconfig.SuSE.10.bfc"}, "raw": {"sha1": "ffd0dfbd1553e15b11649a73a0b3f48318138e0d", "size": 6702, "url": "https://launcher.mojang.com/v1/objects/ffd0dfbd1553e15b11649a73a0b3f48318138e0d/fontconfig.SuSE.10.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.SuSE.10.properties.src": {"downloads": {"lzma": {"sha1": "2c382bd741a9e23114be3da82dee8290ebfca8a9", "size": 1555, "url": "https://launcher.mojang.com/v1/objects/2c382bd741a9e23114be3da82dee8290ebfca8a9/fontconfig.SuSE.10.properties.src"}, "raw": {"sha1": "a38dbdbbc514567b8281e1aea726865f37e97894", "size": 16772, "url": "https://launcher.mojang.com/v1/objects/a38dbdbbc514567b8281e1aea726865f37e97894/fontconfig.SuSE.10.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.SuSE.11.bfc": {"downloads": {"lzma": {"sha1": "2b78cbf11289c9858951fea7180696ba3b7176d6", "size": 1092, "url": "https://launcher.mojang.com/v1/objects/2b78cbf11289c9858951fea7180696ba3b7176d6/fontconfig.SuSE.11.bfc"}, "raw": {"sha1": "a4d8500fcb47f6327460a95851b1368660da8302", "size": 7032, "url": "https://launcher.mojang.com/v1/objects/a4d8500fcb47f6327460a95851b1368660da8302/fontconfig.SuSE.11.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.SuSE.11.properties.src": {"downloads": {"lzma": {"sha1": "5c1635803906e2c59d36492dec724dd7ae49a5ab", "size": 1589, "url": "https://launcher.mojang.com/v1/objects/5c1635803906e2c59d36492dec724dd7ae49a5ab/fontconfig.SuSE.11.properties.src"}, "raw": {"sha1": "c4b69589e41a7279a71866a9134213be19cdf88d", "size": 16781, "url": "https://launcher.mojang.com/v1/objects/c4b69589e41a7279a71866a9134213be19cdf88d/fontconfig.SuSE.11.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.Turbo.bfc": {"downloads": {"lzma": {"sha1": "1c771325d9ee4af209a3db92294451d58962c7a4", "size": 822, "url": "https://launcher.mojang.com/v1/objects/1c771325d9ee4af209a3db92294451d58962c7a4/fontconfig.Turbo.bfc"}, "raw": {"sha1": "f24368deeb85cc9d0781083bc56e773518d72219", "size": 4668, "url": "https://launcher.mojang.com/v1/objects/f24368deeb85cc9d0781083bc56e773518d72219/fontconfig.Turbo.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.Turbo.properties.src": {"downloads": {"lzma": {"sha1": "7748ffa17e2c8a34754138efa963ba39bd1cbbb3", "size": 1113, "url": "https://launcher.mojang.com/v1/objects/7748ffa17e2c8a34754138efa963ba39bd1cbbb3/fontconfig.Turbo.properties.src"}, "raw": {"sha1": "2bb7258bed7ccd4f117e4e5f892c9b13424b0c82", "size": 9192, "url": "https://launcher.mojang.com/v1/objects/2bb7258bed7ccd4f117e4e5f892c9b13424b0c82/fontconfig.Turbo.properties.src"}}, "executable": false, "type": "file"}, "lib/fontconfig.bfc": {"downloads": {"lzma": {"sha1": "be6d49ee8c64f458c4f0e64254963fec48d25150", "size": 286, "url": "https://launcher.mojang.com/v1/objects/be6d49ee8c64f458c4f0e64254963fec48d25150/fontconfig.bfc"}, "raw": {"sha1": "de39b0e19637f58d92a0188122514aa7247ebb5b", "size": 1678, "url": "https://launcher.mojang.com/v1/objects/de39b0e19637f58d92a0188122514aa7247ebb5b/fontconfig.bfc"}}, "executable": false, "type": "file"}, "lib/fontconfig.properties.src": {"downloads": {"lzma": {"sha1": "9498d5e00e5401200667687e826e28c60fa60ba4", "size": 417, "url": "https://launcher.mojang.com/v1/objects/9498d5e00e5401200667687e826e28c60fa60ba4/fontconfig.properties.src"}, "raw": {"sha1": "3617ff1424fd204415242565541facf862b16eb4", "size": 1938, "url": "https://launcher.mojang.com/v1/objects/3617ff1424fd204415242565541facf862b16eb4/fontconfig.properties.src"}}, "executable": false, "type": "file"}, "lib/fonts": {"type": "directory"}, "lib/fonts/LucidaBrightDemiBold.ttf": {"downloads": {"lzma": {"sha1": "4f748750831a7719440dff5457f4d207d0f24d21", "size": 42347, "url": "https://launcher.mojang.com/v1/objects/4f748750831a7719440dff5457f4d207d0f24d21/LucidaBrightDemiBold.ttf"}, "raw": {"sha1": "b5c97f985639e19a3b712193ee48b55dda581fd1", "size": 75144, "url": "https://launcher.mojang.com/v1/objects/b5c97f985639e19a3b712193ee48b55dda581fd1/LucidaBrightDemiBold.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaBrightDemiItalic.ttf": {"downloads": {"lzma": {"sha1": "f82e9a688553c100ecb98412b985807ed56dff5d", "size": 43119, "url": "https://launcher.mojang.com/v1/objects/f82e9a688553c100ecb98412b985807ed56dff5d/LucidaBrightDemiItalic.ttf"}, "raw": {"sha1": "1fd1f757febf3e5f5fbb7fbf7a56587a40d57de7", "size": 75124, "url": "https://launcher.mojang.com/v1/objects/1fd1f757febf3e5f5fbb7fbf7a56587a40d57de7/LucidaBrightDemiItalic.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaBrightItalic.ttf": {"downloads": {"lzma": {"sha1": "6d630df719271319c3d53f90a3d425118b908266", "size": 46206, "url": "https://launcher.mojang.com/v1/objects/6d630df719271319c3d53f90a3d425118b908266/LucidaBrightItalic.ttf"}, "raw": {"sha1": "aa5c037865c563726ecd63d61ca26443589be425", "size": 80856, "url": "https://launcher.mojang.com/v1/objects/aa5c037865c563726ecd63d61ca26443589be425/LucidaBrightItalic.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaBrightRegular.ttf": {"downloads": {"lzma": {"sha1": "4b2e31aaec2238b6ecf9f845bad0a1c6d09fbbfe", "size": 181085, "url": "https://launcher.mojang.com/v1/objects/4b2e31aaec2238b6ecf9f845bad0a1c6d09fbbfe/LucidaBrightRegular.ttf"}, "raw": {"sha1": "5d7ed564791c900a8786936930ba99385653139c", "size": 344908, "url": "https://launcher.mojang.com/v1/objects/5d7ed564791c900a8786936930ba99385653139c/LucidaBrightRegular.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaSansDemiBold.ttf": {"downloads": {"lzma": {"sha1": "079b16dc3c4918ab1f4f760b6dc5e6586c219042", "size": 173229, "url": "https://launcher.mojang.com/v1/objects/079b16dc3c4918ab1f4f760b6dc5e6586c219042/LucidaSansDemiBold.ttf"}, "raw": {"sha1": "92b79fefc35e96190250c602a8fed85276b32a95", "size": 317896, "url": "https://launcher.mojang.com/v1/objects/92b79fefc35e96190250c602a8fed85276b32a95/LucidaSansDemiBold.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaSansRegular.ttf": {"downloads": {"lzma": {"sha1": "64a65d7b94d7153d20957ef6d06bebb4dd0f48e4", "size": 326062, "url": "https://launcher.mojang.com/v1/objects/64a65d7b94d7153d20957ef6d06bebb4dd0f48e4/LucidaSansRegular.ttf"}, "raw": {"sha1": "39cc8bcb8d4a71d4657fc92ef0b9f4e3e9e67add", "size": 698236, "url": "https://launcher.mojang.com/v1/objects/39cc8bcb8d4a71d4657fc92ef0b9f4e3e9e67add/LucidaSansRegular.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaTypewriterBold.ttf": {"downloads": {"lzma": {"sha1": "cdb017f7c34bea0802bc5ea5583aef721ed99c49", "size": 130412, "url": "https://launcher.mojang.com/v1/objects/cdb017f7c34bea0802bc5ea5583aef721ed99c49/LucidaTypewriterBold.ttf"}, "raw": {"sha1": "a5da2eb49448f461470387c939f0e69119310e0b", "size": 234068, "url": "https://launcher.mojang.com/v1/objects/a5da2eb49448f461470387c939f0e69119310e0b/LucidaTypewriterBold.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/LucidaTypewriterRegular.ttf": {"downloads": {"lzma": {"sha1": "aeda4a09a53783b0dc97de8e20071bea874cbfe5", "size": 135184, "url": "https://launcher.mojang.com/v1/objects/aeda4a09a53783b0dc97de8e20071bea874cbfe5/LucidaTypewriterRegular.ttf"}, "raw": {"sha1": "c144dcafe4faf2e79cfd74d8134a631f30234db1", "size": 242700, "url": "https://launcher.mojang.com/v1/objects/c144dcafe4faf2e79cfd74d8134a631f30234db1/LucidaTypewriterRegular.ttf"}}, "executable": false, "type": "file"}, "lib/fonts/fonts.dir": {"downloads": {"lzma": {"sha1": "68f2dd93b215ec8b8d9409d2b9c825632c6b907d", "size": 273, "url": "https://launcher.mojang.com/v1/objects/68f2dd93b215ec8b8d9409d2b9c825632c6b907d/fonts.dir"}, "raw": {"sha1": "97f40cca185c954adf5cc582345a7cb8e4c50578", "size": 4041, "url": "https://launcher.mojang.com/v1/objects/97f40cca185c954adf5cc582345a7cb8e4c50578/fonts.dir"}}, "executable": false, "type": "file"}, "lib/hijrah-config-umalqura.properties": {"downloads": {"lzma": {"sha1": "02e8d296e3b18a450f1ed1547cbf2b7275664c9a", "size": 1969, "url": "https://launcher.mojang.com/v1/objects/02e8d296e3b18a450f1ed1547cbf2b7275664c9a/hijrah-config-umalqura.properties"}, "raw": {"sha1": "84aa425100740722e91f4725caf849e7863d12ba", "size": 13962, "url": "https://launcher.mojang.com/v1/objects/84aa425100740722e91f4725caf849e7863d12ba/hijrah-config-umalqura.properties"}}, "executable": false, "type": "file"}, "lib/images": {"type": "directory"}, "lib/images/cursors": {"type": "directory"}, "lib/images/cursors/cursors.properties": {"downloads": {"lzma": {"sha1": "612bd0f610ee1023947c4a2a8d3fc7d6f97e7d8f", "size": 385, "url": "https://launcher.mojang.com/v1/objects/612bd0f610ee1023947c4a2a8d3fc7d6f97e7d8f/cursors.properties"}, "raw": {"sha1": "f2b9a22ddd0a77869497a64f28f07e89a7d41f48", "size": 1274, "url": "https://launcher.mojang.com/v1/objects/f2b9a22ddd0a77869497a64f28f07e89a7d41f48/cursors.properties"}}, "executable": false, "type": "file"}, "lib/images/cursors/invalid32x32.gif": {"downloads": {"raw": {"sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_CopyDrop32x32.gif": {"downloads": {"raw": {"sha1": "eb7620fae702172aa663a19d170a0b929d3b11d1", "size": 158, "url": "https://launcher.mojang.com/v1/objects/eb7620fae702172aa663a19d170a0b929d3b11d1/motif_CopyDrop32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_CopyNoDrop32x32.gif": {"downloads": {"raw": {"sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_LinkDrop32x32.gif": {"downloads": {"raw": {"sha1": "9699137f990c240e714481563181069c8f6c17bb", "size": 162, "url": "https://launcher.mojang.com/v1/objects/9699137f990c240e714481563181069c8f6c17bb/motif_LinkDrop32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_LinkNoDrop32x32.gif": {"downloads": {"raw": {"sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_MoveDrop32x32.gif": {"downloads": {"raw": {"sha1": "03c1617ce3c5ab8af03e46d30a8c8f31ab57fb1b", "size": 141, "url": "https://launcher.mojang.com/v1/objects/03c1617ce3c5ab8af03e46d30a8c8f31ab57fb1b/motif_MoveDrop32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/cursors/motif_MoveNoDrop32x32.gif": {"downloads": {"raw": {"sha1": "259edc45b4569427e8319895a444f4295d54348f", "size": 153, "url": "https://launcher.mojang.com/v1/objects/259edc45b4569427e8319895a444f4295d54348f/invalid32x32.gif"}}, "executable": false, "type": "file"}, "lib/images/icons": {"type": "directory"}, "lib/images/icons/sun-java.png": {"downloads": {"raw": {"sha1": "d101b693aa054f51097eebdfeed8b8a6ca7b55b8", "size": 4707, "url": "https://launcher.mojang.com/v1/objects/d101b693aa054f51097eebdfeed8b8a6ca7b55b8/sun-java.png"}}, "executable": false, "type": "file"}, "lib/images/icons/sun-java_HighContrast.png": {"downloads": {"raw": {"sha1": "a6b1e418d6b5d03719b96f61f0c5236a60970151", "size": 3729, "url": "https://launcher.mojang.com/v1/objects/a6b1e418d6b5d03719b96f61f0c5236a60970151/sun-java_HighContrast.png"}}, "executable": false, "type": "file"}, "lib/images/icons/sun-java_HighContrastInverse.png": {"downloads": {"raw": {"sha1": "2dda28b9bddc9b5b018e3e8a8b062a99d9b2f887", "size": 3777, "url": "https://launcher.mojang.com/v1/objects/2dda28b9bddc9b5b018e3e8a8b062a99d9b2f887/sun-java_HighContrastInverse.png"}}, "executable": false, "type": "file"}, "lib/images/icons/sun-java_LowContrast.png": {"downloads": {"raw": {"sha1": "7714cc4e894c3626c8da6fe742ed22b2829122d9", "size": 4012, "url": "https://launcher.mojang.com/v1/objects/7714cc4e894c3626c8da6fe742ed22b2829122d9/sun-java_LowContrast.png"}}, "executable": false, "type": "file"}, "lib/javafx.properties": {"downloads": {"raw": {"sha1": "49e6b75d109e5fd3f6cbe7cc5fa9a7980796d14d", "size": 56, "url": "https://launcher.mojang.com/v1/objects/49e6b75d109e5fd3f6cbe7cc5fa9a7980796d14d/javafx.properties"}}, "executable": false, "type": "file"}, "lib/javaws.jar": {"downloads": {"raw": {"sha1": "04fa5ae04ead65b91be5dee575497e49ffd49fe9", "size": 488118, "url": "https://launcher.mojang.com/v1/objects/04fa5ae04ead65b91be5dee575497e49ffd49fe9/javaws.jar"}}, "executable": false, "type": "file"}, "lib/jce.jar": {"downloads": {"raw": {"sha1": "5460adee09cc5fc8829c0acfc46c34670a7d70a0", "size": 115646, "url": "https://launcher.mojang.com/v1/objects/5460adee09cc5fc8829c0acfc46c34670a7d70a0/jce.jar"}}, "executable": false, "type": "file"}, "lib/jexec": {"downloads": {"lzma": {"sha1": "2d4323d4e060f8126d026ca6c03b8972aedd2fab", "size": 3311, "url": "https://launcher.mojang.com/v1/objects/2d4323d4e060f8126d026ca6c03b8972aedd2fab/jexec"}, "raw": {"sha1": "6aa01f1d8d103974164bcfaea03c04eeeefd7d41", "size": 13376, "url": "https://launcher.mojang.com/v1/objects/6aa01f1d8d103974164bcfaea03c04eeeefd7d41/jexec"}}, "executable": true, "type": "file"}, "lib/jfr": {"type": "directory"}, "lib/jfr.jar": {"downloads": {"lzma": {"sha1": "5b9d615c91c72f4fe356d9b4105946679452d1e1", "size": 137982, "url": "https://launcher.mojang.com/v1/objects/5b9d615c91c72f4fe356d9b4105946679452d1e1/jfr.jar"}, "raw": {"sha1": "0f3fd66a336703d935bdc22ad8082bc51d34e534", "size": 560713, "url": "https://launcher.mojang.com/v1/objects/0f3fd66a336703d935bdc22ad8082bc51d34e534/jfr.jar"}}, "executable": false, "type": "file"}, "lib/jfr/default.jfc": {"downloads": {"lzma": {"sha1": "373ddd878146dd8ce8991c2c5115a05a82859bdb", "size": 2207, "url": "https://launcher.mojang.com/v1/objects/373ddd878146dd8ce8991c2c5115a05a82859bdb/default.jfc"}, "raw": {"sha1": "1a64b68d0e7d43f8149faba94440be54f4f24527", "size": 20109, "url": "https://launcher.mojang.com/v1/objects/1a64b68d0e7d43f8149faba94440be54f4f24527/default.jfc"}}, "executable": false, "type": "file"}, "lib/jfr/profile.jfc": {"downloads": {"lzma": {"sha1": "3dcdc5feee3ccfb66bc8726b666944cd4bdadae3", "size": 2199, "url": "https://launcher.mojang.com/v1/objects/3dcdc5feee3ccfb66bc8726b666944cd4bdadae3/profile.jfc"}, "raw": {"sha1": "5d7d08a595f76322c51ae43ea966fbba6b69eebe", "size": 20065, "url": "https://launcher.mojang.com/v1/objects/5d7d08a595f76322c51ae43ea966fbba6b69eebe/profile.jfc"}}, "executable": false, "type": "file"}, "lib/jfxswt.jar": {"downloads": {"raw": {"sha1": "99d9a264c898d84c01e1c42565e7fe1a89dcd72d", "size": 33932, "url": "https://launcher.mojang.com/v1/objects/99d9a264c898d84c01e1c42565e7fe1a89dcd72d/jfxswt.jar"}}, "executable": false, "type": "file"}, "lib/jsse.jar": {"downloads": {"lzma": {"sha1": "94a17dfbc2e76cd12c33970a15341424f875a9ce", "size": 187549, "url": "https://launcher.mojang.com/v1/objects/94a17dfbc2e76cd12c33970a15341424f875a9ce/jsse.jar"}, "raw": {"sha1": "92c5c626e8a2d16f41272c0e404d4f992dd8310a", "size": 675599, "url": "https://launcher.mojang.com/v1/objects/92c5c626e8a2d16f41272c0e404d4f992dd8310a/jsse.jar"}}, "executable": false, "type": "file"}, "lib/jvm.hprof.txt": {"downloads": {"lzma": {"sha1": "eccdb240a815b2a83a502749339b27bb8669965b", "size": 1863, "url": "https://launcher.mojang.com/v1/objects/eccdb240a815b2a83a502749339b27bb8669965b/jvm.hprof.txt"}, "raw": {"sha1": "fbd61d52534cdd0c15df332114d469c65d001e33", "size": 4226, "url": "https://launcher.mojang.com/v1/objects/fbd61d52534cdd0c15df332114d469c65d001e33/jvm.hprof.txt"}}, "executable": false, "type": "file"}, "lib/locale": {"type": "directory"}, "lib/locale/de": {"type": "directory"}, "lib/locale/de/LC_MESSAGES": {"type": "directory"}, "lib/locale/de/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "3061d922907cc557208109088fc6ab81d577ff6f", "size": 970, "url": "https://launcher.mojang.com/v1/objects/3061d922907cc557208109088fc6ab81d577ff6f/sunw_java_plugin.mo"}, "raw": {"sha1": "5b223a3d723ac1cfce63623fb109f2868d47d1b7", "size": 2483, "url": "https://launcher.mojang.com/v1/objects/5b223a3d723ac1cfce63623fb109f2868d47d1b7/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/es": {"type": "directory"}, "lib/locale/es/LC_MESSAGES": {"type": "directory"}, "lib/locale/es/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "24338049a89b323e17182b3a3006b50565d4fa0f", "size": 979, "url": "https://launcher.mojang.com/v1/objects/24338049a89b323e17182b3a3006b50565d4fa0f/sunw_java_plugin.mo"}, "raw": {"sha1": "6cc63dc97f2fdb2ed799e48b1dc98c4f37cdecc1", "size": 2477, "url": "https://launcher.mojang.com/v1/objects/6cc63dc97f2fdb2ed799e48b1dc98c4f37cdecc1/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/fr": {"type": "directory"}, "lib/locale/fr/LC_MESSAGES": {"type": "directory"}, "lib/locale/fr/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "22796a48ef39f57d2d6fa70f41308e493d7f05c1", "size": 1033, "url": "https://launcher.mojang.com/v1/objects/22796a48ef39f57d2d6fa70f41308e493d7f05c1/sunw_java_plugin.mo"}, "raw": {"sha1": "d9d5b458db6e83fdf85c3526aeee3f57c4929840", "size": 2746, "url": "https://launcher.mojang.com/v1/objects/d9d5b458db6e83fdf85c3526aeee3f57c4929840/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/it": {"type": "directory"}, "lib/locale/it/LC_MESSAGES": {"type": "directory"}, "lib/locale/it/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "59a4cae38bfb8927745674d0efc2f284bc277987", "size": 958, "url": "https://launcher.mojang.com/v1/objects/59a4cae38bfb8927745674d0efc2f284bc277987/sunw_java_plugin.mo"}, "raw": {"sha1": "f6e72e3b2141ccc3dffab10ae14a754e494577ba", "size": 2434, "url": "https://launcher.mojang.com/v1/objects/f6e72e3b2141ccc3dffab10ae14a754e494577ba/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/ja": {"type": "directory"}, "lib/locale/ja/LC_MESSAGES": {"type": "directory"}, "lib/locale/ja/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "7d6aeed563e1cefcf0224cf522048468088884a9", "size": 1036, "url": "https://launcher.mojang.com/v1/objects/7d6aeed563e1cefcf0224cf522048468088884a9/sunw_java_plugin.mo"}, "raw": {"sha1": "378881a8cb8dd2aebb43eacd0c68519be4f258b1", "size": 2415, "url": "https://launcher.mojang.com/v1/objects/378881a8cb8dd2aebb43eacd0c68519be4f258b1/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/ko": {"type": "directory"}, "lib/locale/ko.UTF-8": {"type": "directory"}, "lib/locale/ko.UTF-8/LC_MESSAGES": {"type": "directory"}, "lib/locale/ko.UTF-8/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "12ee3b21511e8497d95ea0ba9d6fe519227d0b16", "size": 1069, "url": "https://launcher.mojang.com/v1/objects/12ee3b21511e8497d95ea0ba9d6fe519227d0b16/sunw_java_plugin.mo"}, "raw": {"sha1": "cb19df01c59662dbe2f4050b1290d374b82fe1fa", "size": 2753, "url": "https://launcher.mojang.com/v1/objects/cb19df01c59662dbe2f4050b1290d374b82fe1fa/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/ko/LC_MESSAGES": {"type": "directory"}, "lib/locale/ko/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "6e2e47c64c360517fd436bc79c823b5679a1efe6", "size": 996, "url": "https://launcher.mojang.com/v1/objects/6e2e47c64c360517fd436bc79c823b5679a1efe6/sunw_java_plugin.mo"}, "raw": {"sha1": "12c8a118d150c78f719314df6dec49a967af71e9", "size": 2399, "url": "https://launcher.mojang.com/v1/objects/12c8a118d150c78f719314df6dec49a967af71e9/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/pt_BR": {"type": "directory"}, "lib/locale/pt_BR/LC_MESSAGES": {"type": "directory"}, "lib/locale/pt_BR/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "bcaa7e7916493f071f1bf64bf58c6b038e3569c9", "size": 940, "url": "https://launcher.mojang.com/v1/objects/bcaa7e7916493f071f1bf64bf58c6b038e3569c9/sunw_java_plugin.mo"}, "raw": {"sha1": "a3bc0c43994c53c59bba94982cf95f6d36283dd0", "size": 2420, "url": "https://launcher.mojang.com/v1/objects/a3bc0c43994c53c59bba94982cf95f6d36283dd0/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/sv": {"type": "directory"}, "lib/locale/sv/LC_MESSAGES": {"type": "directory"}, "lib/locale/sv/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "76017835d6261fe2eedbcbe5eb08a7484c3080c5", "size": 946, "url": "https://launcher.mojang.com/v1/objects/76017835d6261fe2eedbcbe5eb08a7484c3080c5/sunw_java_plugin.mo"}, "raw": {"sha1": "09a47686edec4bbb34e82fbd08559f8bb6266544", "size": 2359, "url": "https://launcher.mojang.com/v1/objects/09a47686edec4bbb34e82fbd08559f8bb6266544/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh": {"type": "directory"}, "lib/locale/zh.GBK": {"type": "directory"}, "lib/locale/zh.GBK/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh.GBK/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "75fd04045bf5890b8bb822770bfdb90a2e9ea65b", "size": 902, "url": "https://launcher.mojang.com/v1/objects/75fd04045bf5890b8bb822770bfdb90a2e9ea65b/sunw_java_plugin.mo"}, "raw": {"sha1": "7006fe7767b8807441a1f359a90509b3e507b0d1", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7006fe7767b8807441a1f359a90509b3e507b0d1/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "75fd04045bf5890b8bb822770bfdb90a2e9ea65b", "size": 902, "url": "https://launcher.mojang.com/v1/objects/75fd04045bf5890b8bb822770bfdb90a2e9ea65b/sunw_java_plugin.mo"}, "raw": {"sha1": "7006fe7767b8807441a1f359a90509b3e507b0d1", "size": 2002, "url": "https://launcher.mojang.com/v1/objects/7006fe7767b8807441a1f359a90509b3e507b0d1/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh_HK.BIG5HK": {"type": "directory"}, "lib/locale/zh_HK.BIG5HK/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh_HK.BIG5HK/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "3a1397bb1b1741697be1479232b6d9599940c851", "size": 912, "url": "https://launcher.mojang.com/v1/objects/3a1397bb1b1741697be1479232b6d9599940c851/sunw_java_plugin.mo"}, "raw": {"sha1": "c6023544067278c78599921f1032de353ff7da42", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/c6023544067278c78599921f1032de353ff7da42/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh_TW": {"type": "directory"}, "lib/locale/zh_TW.BIG5": {"type": "directory"}, "lib/locale/zh_TW.BIG5/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh_TW.BIG5/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "3a1397bb1b1741697be1479232b6d9599940c851", "size": 912, "url": "https://launcher.mojang.com/v1/objects/3a1397bb1b1741697be1479232b6d9599940c851/sunw_java_plugin.mo"}, "raw": {"sha1": "c6023544067278c78599921f1032de353ff7da42", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/c6023544067278c78599921f1032de353ff7da42/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/locale/zh_TW/LC_MESSAGES": {"type": "directory"}, "lib/locale/zh_TW/LC_MESSAGES/sunw_java_plugin.mo": {"downloads": {"lzma": {"sha1": "c05e610e75182f0c4e77f3e7a4d9670ed62bf63c", "size": 897, "url": "https://launcher.mojang.com/v1/objects/c05e610e75182f0c4e77f3e7a4d9670ed62bf63c/sunw_java_plugin.mo"}, "raw": {"sha1": "f9b972dd059eae3cd337dfcef6a178e8ed8a7db6", "size": 2025, "url": "https://launcher.mojang.com/v1/objects/f9b972dd059eae3cd337dfcef6a178e8ed8a7db6/sunw_java_plugin.mo"}}, "executable": false, "type": "file"}, "lib/logging.properties": {"downloads": {"lzma": {"sha1": "642202a58e5216d086ad37c0b5a633be802edc78", "size": 896, "url": "https://launcher.mojang.com/v1/objects/642202a58e5216d086ad37c0b5a633be802edc78/logging.properties"}, "raw": {"sha1": "89da8094484891f9ec1fa40c6c8b61f94c5869d0", "size": 2455, "url": "https://launcher.mojang.com/v1/objects/89da8094484891f9ec1fa40c6c8b61f94c5869d0/logging.properties"}}, "executable": false, "type": "file"}, "lib/management": {"type": "directory"}, "lib/management-agent.jar": {"downloads": {"lzma": {"sha1": "3ea0bf17e14b3428296a0f4011bf4025fcbfa4bd", "size": 243, "url": "https://launcher.mojang.com/v1/objects/3ea0bf17e14b3428296a0f4011bf4025fcbfa4bd/management-agent.jar"}, "raw": {"sha1": "9fbed36522aa3a80bac08a328942cbc5ef39ca8e", "size": 381, "url": "https://launcher.mojang.com/v1/objects/9fbed36522aa3a80bac08a328942cbc5ef39ca8e/management-agent.jar"}}, "executable": false, "type": "file"}, "lib/management/jmxremote.access": {"downloads": {"lzma": {"sha1": "69042ff1b14165db19c9d728614639dec16d6a31", "size": 1419, "url": "https://launcher.mojang.com/v1/objects/69042ff1b14165db19c9d728614639dec16d6a31/jmxremote.access"}, "raw": {"sha1": "21200eaad898ba4a2a8834a032efb6616fabb930", "size": 3998, "url": "https://launcher.mojang.com/v1/objects/21200eaad898ba4a2a8834a032efb6616fabb930/jmxremote.access"}}, "executable": false, "type": "file"}, "lib/management/jmxremote.password.template": {"downloads": {"lzma": {"sha1": "556c64b1e920766f8867be3964de6e49f5b81a60", "size": 1129, "url": "https://launcher.mojang.com/v1/objects/556c64b1e920766f8867be3964de6e49f5b81a60/jmxremote.password.template"}, "raw": {"sha1": "c1e0f01408bf20fbbb8b4810520c725f70050db5", "size": 2856, "url": "https://launcher.mojang.com/v1/objects/c1e0f01408bf20fbbb8b4810520c725f70050db5/jmxremote.password.template"}}, "executable": false, "type": "file"}, "lib/management/management.properties": {"downloads": {"lzma": {"sha1": "3e52f9baa6394ca6956845424c607e5cde5d3c67", "size": 3176, "url": "https://launcher.mojang.com/v1/objects/3e52f9baa6394ca6956845424c607e5cde5d3c67/management.properties"}, "raw": {"sha1": "e0451d8d7d9e84d7b1c39ec7d00993307a5cbbf1", "size": 14630, "url": "https://launcher.mojang.com/v1/objects/e0451d8d7d9e84d7b1c39ec7d00993307a5cbbf1/management.properties"}}, "executable": false, "type": "file"}, "lib/management/snmp.acl.template": {"downloads": {"lzma": {"sha1": "9a4aa6396c3b488b0663bed5e5ecb762985669c9", "size": 1121, "url": "https://launcher.mojang.com/v1/objects/9a4aa6396c3b488b0663bed5e5ecb762985669c9/snmp.acl.template"}, "raw": {"sha1": "2e9f9ac287274532eb1f0d1afcefd7f3e97cc794", "size": 3376, "url": "https://launcher.mojang.com/v1/objects/2e9f9ac287274532eb1f0d1afcefd7f3e97cc794/snmp.acl.template"}}, "executable": false, "type": "file"}, "lib/meta-index": {"downloads": {"lzma": {"sha1": "1ac60b31362fda4725c665b591c5fbe384cbc8c1", "size": 788, "url": "https://launcher.mojang.com/v1/objects/1ac60b31362fda4725c665b591c5fbe384cbc8c1/meta-index"}, "raw": {"sha1": "bf204f09242203e713c31785158a0792f9edb600", "size": 2034, "url": "https://launcher.mojang.com/v1/objects/bf204f09242203e713c31785158a0792f9edb600/meta-index"}}, "executable": false, "type": "file"}, "lib/net.properties": {"downloads": {"lzma": {"sha1": "e9ec3981a0797bf55bb87b24d9eb651ce7e6916b", "size": 1830, "url": "https://launcher.mojang.com/v1/objects/e9ec3981a0797bf55bb87b24d9eb651ce7e6916b/net.properties"}, "raw": {"sha1": "fd9471742eb759f4478bb1de9a0dc0527265b6ea", "size": 5352, "url": "https://launcher.mojang.com/v1/objects/fd9471742eb759f4478bb1de9a0dc0527265b6ea/net.properties"}}, "executable": false, "type": "file"}, "lib/oblique-fonts": {"type": "directory"}, "lib/oblique-fonts/LucidaSansDemiOblique.ttf": {"downloads": {"lzma": {"sha1": "49c8980c1b89bbdbab59d0f5bd5bebf0afcb93b2", "size": 38580, "url": "https://launcher.mojang.com/v1/objects/49c8980c1b89bbdbab59d0f5bd5bebf0afcb93b2/LucidaSansDemiOblique.ttf"}, "raw": {"sha1": "53e4e12a675ac222469341c3dbc102464a1be4c7", "size": 91352, "url": "https://launcher.mojang.com/v1/objects/53e4e12a675ac222469341c3dbc102464a1be4c7/LucidaSansDemiOblique.ttf"}}, "executable": false, "type": "file"}, "lib/oblique-fonts/LucidaSansOblique.ttf": {"downloads": {"lzma": {"sha1": "553123c0edcd08035dede4ffd92b5b81c9a7538a", "size": 116575, "url": "https://launcher.mojang.com/v1/objects/553123c0edcd08035dede4ffd92b5b81c9a7538a/LucidaSansOblique.ttf"}, "raw": {"sha1": "95a195ad4fc520b3e395c85b747fc3024d118dd9", "size": 253724, "url": "https://launcher.mojang.com/v1/objects/95a195ad4fc520b3e395c85b747fc3024d118dd9/LucidaSansOblique.ttf"}}, "executable": false, "type": "file"}, "lib/oblique-fonts/LucidaTypewriterBoldOblique.ttf": {"downloads": {"lzma": {"sha1": "2475b08151556ad4d89bb1d2b6494c6bee9abd82", "size": 29954, "url": "https://launcher.mojang.com/v1/objects/2475b08151556ad4d89bb1d2b6494c6bee9abd82/LucidaTypewriterBoldOblique.ttf"}, "raw": {"sha1": "f331fc8b0cc494702bc46b690f2b8eed36469a02", "size": 63168, "url": "https://launcher.mojang.com/v1/objects/f331fc8b0cc494702bc46b690f2b8eed36469a02/LucidaTypewriterBoldOblique.ttf"}}, "executable": false, "type": "file"}, "lib/oblique-fonts/LucidaTypewriterOblique.ttf": {"downloads": {"lzma": {"sha1": "5b970bc3b7abb21dce1aa28ff7f03459d351e552", "size": 60133, "url": "https://launcher.mojang.com/v1/objects/5b970bc3b7abb21dce1aa28ff7f03459d351e552/LucidaTypewriterOblique.ttf"}, "raw": {"sha1": "f8ea00db73f8a89a27674d050edc37c2280930e1", "size": 137484, "url": "https://launcher.mojang.com/v1/objects/f8ea00db73f8a89a27674d050edc37c2280930e1/LucidaTypewriterOblique.ttf"}}, "executable": false, "type": "file"}, "lib/oblique-fonts/fonts.dir": {"downloads": {"lzma": {"sha1": "067528c789bd713c7c3f34e779aa6e2e8253dcf6", "size": 188, "url": "https://launcher.mojang.com/v1/objects/067528c789bd713c7c3f34e779aa6e2e8253dcf6/fonts.dir"}, "raw": {"sha1": "5aee54ffba9e33de56fd84ef64fa496b898585bb", "size": 2115, "url": "https://launcher.mojang.com/v1/objects/5aee54ffba9e33de56fd84ef64fa496b898585bb/fonts.dir"}}, "executable": false, "type": "file"}, "lib/plugin.jar": {"downloads": {"raw": {"sha1": "3f250842c79112bae5369e372025b166990820e8", "size": 950772, "url": "https://launcher.mojang.com/v1/objects/3f250842c79112bae5369e372025b166990820e8/plugin.jar"}}, "executable": false, "type": "file"}, "lib/psfont.properties.ja": {"downloads": {"lzma": {"sha1": "7ca1cc244ed251cd1eb2347f1eea37d7d18c8ad4", "size": 701, "url": "https://launcher.mojang.com/v1/objects/7ca1cc244ed251cd1eb2347f1eea37d7d18c8ad4/psfont.properties.ja"}, "raw": {"sha1": "56ed1c661eeede17b4fae8c9de7b5edbad387abc", "size": 2796, "url": "https://launcher.mojang.com/v1/objects/56ed1c661eeede17b4fae8c9de7b5edbad387abc/psfont.properties.ja"}}, "executable": false, "type": "file"}, "lib/psfontj2d.properties": {"downloads": {"lzma": {"sha1": "4252fa01af8739a3545e2b705e3383892e22ab40", "size": 2278, "url": "https://launcher.mojang.com/v1/objects/4252fa01af8739a3545e2b705e3383892e22ab40/psfontj2d.properties"}, "raw": {"sha1": "aa327a22a49967f4d74afeee6726f505f209692f", "size": 10393, "url": "https://launcher.mojang.com/v1/objects/aa327a22a49967f4d74afeee6726f505f209692f/psfontj2d.properties"}}, "executable": false, "type": "file"}, "lib/resources.jar": {"downloads": {"lzma": {"sha1": "1b0e08441750dc17efe4b527aa146da6cc14e8a6", "size": 579294, "url": "https://launcher.mojang.com/v1/objects/1b0e08441750dc17efe4b527aa146da6cc14e8a6/resources.jar"}, "raw": {"sha1": "daa021906e4648d4c37e798c11733dc2047f2da1", "size": 3505206, "url": "https://launcher.mojang.com/v1/objects/daa021906e4648d4c37e798c11733dc2047f2da1/resources.jar"}}, "executable": false, "type": "file"}, "lib/rt.jar": {"downloads": {"lzma": {"sha1": "fc4a8681aeda29c2a2a3fd11bad7729543283f3d", "size": 14378994, "url": "https://launcher.mojang.com/v1/objects/fc4a8681aeda29c2a2a3fd11bad7729543283f3d/rt.jar"}, "raw": {"sha1": "5396b0954a20f3210f1f4f1886ead30880d6ebfe", "size": 66334986, "url": "https://launcher.mojang.com/v1/objects/5396b0954a20f3210f1f4f1886ead30880d6ebfe/rt.jar"}}, "executable": false, "type": "file"}, "lib/security": {"type": "directory"}, "lib/security/blacklist": {"downloads": {"lzma": {"sha1": "8206fce6c1d91a39fdf78e8e79e953913994a1cd", "size": 1969, "url": "https://launcher.mojang.com/v1/objects/8206fce6c1d91a39fdf78e8e79e953913994a1cd/blacklist"}, "raw": {"sha1": "d4ffb3857eab403955ce9d156e46d056061e6a5a", "size": 4054, "url": "https://launcher.mojang.com/v1/objects/d4ffb3857eab403955ce9d156e46d056061e6a5a/blacklist"}}, "executable": false, "type": "file"}, "lib/security/blacklisted.certs": {"downloads": {"lzma": {"sha1": "8311bead054caf6cfe678d4b7998de4caaabfa53", "size": 806, "url": "https://launcher.mojang.com/v1/objects/8311bead054caf6cfe678d4b7998de4caaabfa53/blacklisted.certs"}, "raw": {"sha1": "c5c005c29a80493f5c31cd7eb629ac1b9c752404", "size": 1273, "url": "https://launcher.mojang.com/v1/objects/c5c005c29a80493f5c31cd7eb629ac1b9c752404/blacklisted.certs"}}, "executable": false, "type": "file"}, "lib/security/cacerts": {"downloads": {"lzma": {"sha1": "654dd94809655d5b28385cbb5eba8d6ad9f2c1aa", "size": 67802, "url": "https://launcher.mojang.com/v1/objects/654dd94809655d5b28385cbb5eba8d6ad9f2c1aa/cacerts"}, "raw": {"sha1": "2917859c443c68e19f93abcd1315c3c2904cbef9", "size": 104430, "url": "https://launcher.mojang.com/v1/objects/2917859c443c68e19f93abcd1315c3c2904cbef9/cacerts"}}, "executable": false, "type": "file"}, "lib/security/java.policy": {"downloads": {"lzma": {"sha1": "b601c420d02ef3dbd8595453d08fdef91134e8b5", "size": 647, "url": "https://launcher.mojang.com/v1/objects/b601c420d02ef3dbd8595453d08fdef91134e8b5/java.policy"}, "raw": {"sha1": "c0112209a567b3b523cfed7041709f9440227968", "size": 2466, "url": "https://launcher.mojang.com/v1/objects/c0112209a567b3b523cfed7041709f9440227968/java.policy"}}, "executable": false, "type": "file"}, "lib/security/java.security": {"downloads": {"lzma": {"sha1": "531620e82ca0365ce8dc97096bb0ac5a7ace5952", "size": 10959, "url": "https://launcher.mojang.com/v1/objects/531620e82ca0365ce8dc97096bb0ac5a7ace5952/java.security"}, "raw": {"sha1": "5dcc17a168c53d0b366784e520bd4d55aa61ac18", "size": 41528, "url": "https://launcher.mojang.com/v1/objects/5dcc17a168c53d0b366784e520bd4d55aa61ac18/java.security"}}, "executable": false, "type": "file"}, "lib/security/javaws.policy": {"downloads": {"raw": {"sha1": "4384ca5e4d32f7dd86d8baddd1e690730d74e694", "size": 98, "url": "https://launcher.mojang.com/v1/objects/4384ca5e4d32f7dd86d8baddd1e690730d74e694/javaws.policy"}}, "executable": false, "type": "file"}, "lib/security/policy": {"type": "directory"}, "lib/security/policy/limited": {"type": "directory"}, "lib/security/policy/limited/US_export_policy.jar": {"downloads": {"raw": {"sha1": "7d69ea3b385bc067738520f1b5c549e1084be285", "size": 3026, "url": "https://launcher.mojang.com/v1/objects/7d69ea3b385bc067738520f1b5c549e1084be285/US_export_policy.jar"}}, "executable": false, "type": "file"}, "lib/security/policy/limited/local_policy.jar": {"downloads": {"raw": {"sha1": "238b8826e110f58acb2e1959773b0a577cd4d569", "size": 3527, "url": "https://launcher.mojang.com/v1/objects/238b8826e110f58acb2e1959773b0a577cd4d569/local_policy.jar"}}, "executable": false, "type": "file"}, "lib/security/policy/unlimited": {"type": "directory"}, "lib/security/policy/unlimited/US_export_policy.jar": {"downloads": {"raw": {"sha1": "f6fb2af1e87fc622cda194a7d6b5f5f069653ff1", "size": 3023, "url": "https://launcher.mojang.com/v1/objects/f6fb2af1e87fc622cda194a7d6b5f5f069653ff1/US_export_policy.jar"}}, "executable": false, "type": "file"}, "lib/security/policy/unlimited/local_policy.jar": {"downloads": {"raw": {"sha1": "517368ab2cbaf6b42ea0b963f98eeedd996e83e3", "size": 3035, "url": "https://launcher.mojang.com/v1/objects/517368ab2cbaf6b42ea0b963f98eeedd996e83e3/local_policy.jar"}}, "executable": false, "type": "file"}, "lib/security/trusted.libraries": {"downloads": {"raw": {"sha1": "da39a3ee5e6b4b0d3255bfef95601890afd80709", "size": 0, "url": "https://launcher.mojang.com/v1/objects/da39a3ee5e6b4b0d3255bfef95601890afd80709/trusted.libraries"}}, "executable": false, "type": "file"}, "lib/sound.properties": {"downloads": {"lzma": {"sha1": "3b5f7e4ec437d79048af35094290577f483b3fe1", "size": 473, "url": "https://launcher.mojang.com/v1/objects/3b5f7e4ec437d79048af35094290577f483b3fe1/sound.properties"}, "raw": {"sha1": "9afceb218059d981d0fa9f07aad3c5097cf41b0c", "size": 1210, "url": "https://launcher.mojang.com/v1/objects/9afceb218059d981d0fa9f07aad3c5097cf41b0c/sound.properties"}}, "executable": false, "type": "file"}, "lib/tzdb.dat": {"downloads": {"lzma": {"sha1": "39c69339965484afe89c14111baeeb862fdefd97", "size": 32547, "url": "https://launcher.mojang.com/v1/objects/39c69339965484afe89c14111baeeb862fdefd97/tzdb.dat"}, "raw": {"sha1": "b59c07e3619271a3b9861e999f4b138e971baf69", "size": 105734, "url": "https://launcher.mojang.com/v1/objects/b59c07e3619271a3b9861e999f4b138e971baf69/tzdb.dat"}}, "executable": false, "type": "file"}, "man": {"type": "directory"}, "man/ja": {"target": "ja_JP.UTF-8", "type": "link"}, "man/ja_JP.UTF-8": {"type": "directory"}, "man/ja_JP.UTF-8/man1": {"type": "directory"}, "man/ja_JP.UTF-8/man1/java.1": {"downloads": {"lzma": {"sha1": "f9da09710b6c6df23c256e324a0c4df00a0d6ded", "size": 25461, "url": "https://launcher.mojang.com/v1/objects/f9da09710b6c6df23c256e324a0c4df00a0d6ded/java.1"}, "raw": {"sha1": "b0b12a0bb66e6171771ca4b1dfca32fb759bcaec", "size": 148688, "url": "https://launcher.mojang.com/v1/objects/b0b12a0bb66e6171771ca4b1dfca32fb759bcaec/java.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/javaws.1": {"downloads": {"lzma": {"sha1": "6188fae453ca09ccb19be5c9f4d2059926b36267", "size": 2154, "url": "https://launcher.mojang.com/v1/objects/6188fae453ca09ccb19be5c9f4d2059926b36267/javaws.1"}, "raw": {"sha1": "8f39d928870268ace07bedfebd18db1e1d07fc37", "size": 6641, "url": "https://launcher.mojang.com/v1/objects/8f39d928870268ace07bedfebd18db1e1d07fc37/javaws.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/jjs.1": {"downloads": {"lzma": {"sha1": "6e42b989d28b185dc1aab50c0389834e649a37d4", "size": 3452, "url": "https://launcher.mojang.com/v1/objects/6e42b989d28b185dc1aab50c0389834e649a37d4/jjs.1"}, "raw": {"sha1": "e023322a2013912315a2bd1034e6f829a27c76e0", "size": 11365, "url": "https://launcher.mojang.com/v1/objects/e023322a2013912315a2bd1034e6f829a27c76e0/jjs.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/keytool.1": {"downloads": {"lzma": {"sha1": "a78134a4bddd53d684a70aa677e51a215db1c9cb", "size": 20698, "url": "https://launcher.mojang.com/v1/objects/a78134a4bddd53d684a70aa677e51a215db1c9cb/keytool.1"}, "raw": {"sha1": "148583c837eaaf6333ccfd8c9e8df08574e14b0c", "size": 111033, "url": "https://launcher.mojang.com/v1/objects/148583c837eaaf6333ccfd8c9e8df08574e14b0c/keytool.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/orbd.1": {"downloads": {"lzma": {"sha1": "326af0dcbff173ef8aee29163dbe146d7389cc3e", "size": 4225, "url": "https://launcher.mojang.com/v1/objects/326af0dcbff173ef8aee29163dbe146d7389cc3e/orbd.1"}, "raw": {"sha1": "95651622d33c08286858ec337edd3ea72acd93dc", "size": 16092, "url": "https://launcher.mojang.com/v1/objects/95651622d33c08286858ec337edd3ea72acd93dc/orbd.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/pack200.1": {"downloads": {"lzma": {"sha1": "e0eedafa748c61a44e5be4355fe9d44b05048e80", "size": 4293, "url": "https://launcher.mojang.com/v1/objects/e0eedafa748c61a44e5be4355fe9d44b05048e80/pack200.1"}, "raw": {"sha1": "aa21a0ab75707f7fc66e83c7a392e69b37ddf80e", "size": 14482, "url": "https://launcher.mojang.com/v1/objects/aa21a0ab75707f7fc66e83c7a392e69b37ddf80e/pack200.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/policytool.1": {"downloads": {"lzma": {"sha1": "3c766ed12dab58166169d35680c392a6be1814a1", "size": 1380, "url": "https://launcher.mojang.com/v1/objects/3c766ed12dab58166169d35680c392a6be1814a1/policytool.1"}, "raw": {"sha1": "80879c74e072a98fad6f32b3283331aaf9bd002f", "size": 4020, "url": "https://launcher.mojang.com/v1/objects/80879c74e072a98fad6f32b3283331aaf9bd002f/policytool.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/rmid.1": {"downloads": {"lzma": {"sha1": "1e20779d990beacc32a48237777d670fcc47ca14", "size": 4836, "url": "https://launcher.mojang.com/v1/objects/1e20779d990beacc32a48237777d670fcc47ca14/rmid.1"}, "raw": {"sha1": "7e40cb8003d098d6e36f45640b26f979ac94b5c5", "size": 19715, "url": "https://launcher.mojang.com/v1/objects/7e40cb8003d098d6e36f45640b26f979ac94b5c5/rmid.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/rmiregistry.1": {"downloads": {"lzma": {"sha1": "aaf4ffe07e954f8696eef1ecb7a5e244628d0ad9", "size": 1627, "url": "https://launcher.mojang.com/v1/objects/aaf4ffe07e954f8696eef1ecb7a5e244628d0ad9/rmiregistry.1"}, "raw": {"sha1": "c53c52f3ae7a011c135894c9fc51b741e729c33d", "size": 4557, "url": "https://launcher.mojang.com/v1/objects/c53c52f3ae7a011c135894c9fc51b741e729c33d/rmiregistry.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/servertool.1": {"downloads": {"lzma": {"sha1": "3b9e624e9d1cf2959b438a35061162e2100ddecd", "size": 2626, "url": "https://launcher.mojang.com/v1/objects/3b9e624e9d1cf2959b438a35061162e2100ddecd/servertool.1"}, "raw": {"sha1": "50ab8bcd9dd9d0b1a3d81348fbce1c8f82e7189e", "size": 9081, "url": "https://launcher.mojang.com/v1/objects/50ab8bcd9dd9d0b1a3d81348fbce1c8f82e7189e/servertool.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/tnameserv.1": {"downloads": {"lzma": {"sha1": "bb3106ff74c60a76de3d20659b9c2128c70f3bf2", "size": 4478, "url": "https://launcher.mojang.com/v1/objects/bb3106ff74c60a76de3d20659b9c2128c70f3bf2/tnameserv.1"}, "raw": {"sha1": "01e714671ecd1167edcb5310b16a9c59c33c3eaa", "size": 17722, "url": "https://launcher.mojang.com/v1/objects/01e714671ecd1167edcb5310b16a9c59c33c3eaa/tnameserv.1"}}, "executable": false, "type": "file"}, "man/ja_JP.UTF-8/man1/unpack200.1": {"downloads": {"lzma": {"sha1": "c115a881cf800b08df294df55d9f250ae944e33c", "size": 1973, "url": "https://launcher.mojang.com/v1/objects/c115a881cf800b08df294df55d9f250ae944e33c/unpack200.1"}, "raw": {"sha1": "7c882bba0067367a41ad84868d18793b8a7397a3", "size": 5382, "url": "https://launcher.mojang.com/v1/objects/7c882bba0067367a41ad84868d18793b8a7397a3/unpack200.1"}}, "executable": false, "type": "file"}, "man/man1": {"type": "directory"}, "man/man1/java.1": {"downloads": {"lzma": {"sha1": "06a6b0275c202bf698d73ca71f95618d56d81c15", "size": 25796, "url": "https://launcher.mojang.com/v1/objects/06a6b0275c202bf698d73ca71f95618d56d81c15/java.1"}, "raw": {"sha1": "69fec7a341aa91f18dbdcdb95952dede7e1b689a", "size": 124796, "url": "https://launcher.mojang.com/v1/objects/69fec7a341aa91f18dbdcdb95952dede7e1b689a/java.1"}}, "executable": false, "type": "file"}, "man/man1/javaws.1": {"downloads": {"lzma": {"sha1": "4bae251c6dfb5420f56928815cf80d0b6d517a1f", "size": 1759, "url": "https://launcher.mojang.com/v1/objects/4bae251c6dfb5420f56928815cf80d0b6d517a1f/javaws.1"}, "raw": {"sha1": "e61e44e101b1bc119c2d2d4b10320f38b36a8036", "size": 4897, "url": "https://launcher.mojang.com/v1/objects/e61e44e101b1bc119c2d2d4b10320f38b36a8036/javaws.1"}}, "executable": false, "type": "file"}, "man/man1/jjs.1": {"downloads": {"lzma": {"sha1": "29683cf2bd47015c9461b688749ddffd95f6671d", "size": 1881, "url": "https://launcher.mojang.com/v1/objects/29683cf2bd47015c9461b688749ddffd95f6671d/jjs.1"}, "raw": {"sha1": "78d419bd3a7f3e0802d5220e690429194b5d1beb", "size": 4932, "url": "https://launcher.mojang.com/v1/objects/78d419bd3a7f3e0802d5220e690429194b5d1beb/jjs.1"}}, "executable": false, "type": "file"}, "man/man1/keytool.1": {"downloads": {"lzma": {"sha1": "b67e5126d43713ee3675706724b34061578b42db", "size": 19690, "url": "https://launcher.mojang.com/v1/objects/b67e5126d43713ee3675706724b34061578b42db/keytool.1"}, "raw": {"sha1": "4c976f86057ab779763fcfb98f5702ebef47f629", "size": 86925, "url": "https://launcher.mojang.com/v1/objects/4c976f86057ab779763fcfb98f5702ebef47f629/keytool.1"}}, "executable": false, "type": "file"}, "man/man1/orbd.1": {"downloads": {"lzma": {"sha1": "147064d6f7e027002e296bb246ae572d0ce0495b", "size": 3708, "url": "https://launcher.mojang.com/v1/objects/147064d6f7e027002e296bb246ae572d0ce0495b/orbd.1"}, "raw": {"sha1": "64201e1846fcf1dcc45c786ffeab89426d1c7742", "size": 12180, "url": "https://launcher.mojang.com/v1/objects/64201e1846fcf1dcc45c786ffeab89426d1c7742/orbd.1"}}, "executable": false, "type": "file"}, "man/man1/pack200.1": {"downloads": {"lzma": {"sha1": "fe17486bbe9c58cf4182fa056b9cd124e8295607", "size": 3724, "url": "https://launcher.mojang.com/v1/objects/fe17486bbe9c58cf4182fa056b9cd124e8295607/pack200.1"}, "raw": {"sha1": "26826cf52b89924f2d2a60d6cda798891875eae6", "size": 11623, "url": "https://launcher.mojang.com/v1/objects/26826cf52b89924f2d2a60d6cda798891875eae6/pack200.1"}}, "executable": false, "type": "file"}, "man/man1/policytool.1": {"downloads": {"lzma": {"sha1": "bd154e7c39aca71d15b2098c588866f8d95bc743", "size": 1122, "url": "https://launcher.mojang.com/v1/objects/bd154e7c39aca71d15b2098c588866f8d95bc743/policytool.1"}, "raw": {"sha1": "ab296625155d9a2b25ecc2b4feff2f741b3ad136", "size": 3235, "url": "https://launcher.mojang.com/v1/objects/ab296625155d9a2b25ecc2b4feff2f741b3ad136/policytool.1"}}, "executable": false, "type": "file"}, "man/man1/rmid.1": {"downloads": {"lzma": {"sha1": "6a7da234e7f43ebca5c4ba8cd862fda3be62fbaa", "size": 4255, "url": "https://launcher.mojang.com/v1/objects/6a7da234e7f43ebca5c4ba8cd862fda3be62fbaa/rmid.1"}, "raw": {"sha1": "6f10e214d7950a6a8460524e41dc700f112f89e5", "size": 15979, "url": "https://launcher.mojang.com/v1/objects/6f10e214d7950a6a8460524e41dc700f112f89e5/rmid.1"}}, "executable": false, "type": "file"}, "man/man1/rmiregistry.1": {"downloads": {"lzma": {"sha1": "f40dd17e3a734600ad1828b0c42d3a1685c4c520", "size": 1301, "url": "https://launcher.mojang.com/v1/objects/f40dd17e3a734600ad1828b0c42d3a1685c4c520/rmiregistry.1"}, "raw": {"sha1": "d9a3d23fab689df5bb9a792b88f462f939b49f70", "size": 3449, "url": "https://launcher.mojang.com/v1/objects/d9a3d23fab689df5bb9a792b88f462f939b49f70/rmiregistry.1"}}, "executable": false, "type": "file"}, "man/man1/servertool.1": {"downloads": {"lzma": {"sha1": "74f1e10712202cd3ca0ff5833de05b7ee67092e1", "size": 2307, "url": "https://launcher.mojang.com/v1/objects/74f1e10712202cd3ca0ff5833de05b7ee67092e1/servertool.1"}, "raw": {"sha1": "e6c7b510740ac8681a9bfb5f4ee1f0306125b728", "size": 7237, "url": "https://launcher.mojang.com/v1/objects/e6c7b510740ac8681a9bfb5f4ee1f0306125b728/servertool.1"}}, "executable": false, "type": "file"}, "man/man1/tnameserv.1": {"downloads": {"lzma": {"sha1": "4bec7f4e070d023f124f9352a8971d7acd249a15", "size": 3955, "url": "https://launcher.mojang.com/v1/objects/4bec7f4e070d023f124f9352a8971d7acd249a15/tnameserv.1"}, "raw": {"sha1": "a31dbbe800d49cb371fab9a4b73d22c3bf8799ad", "size": 15747, "url": "https://launcher.mojang.com/v1/objects/a31dbbe800d49cb371fab9a4b73d22c3bf8799ad/tnameserv.1"}}, "executable": false, "type": "file"}, "man/man1/unpack200.1": {"downloads": {"lzma": {"sha1": "f8e73863187929debf2ea6dadefb2995ec7917e7", "size": 1672, "url": "https://launcher.mojang.com/v1/objects/f8e73863187929debf2ea6dadefb2995ec7917e7/unpack200.1"}, "raw": {"sha1": "437f7233d738cb9b822e99003127049005663e0f", "size": 4244, "url": "https://launcher.mojang.com/v1/objects/437f7233d738cb9b822e99003127049005663e0f/unpack200.1"}}, "executable": false, "type": "file"}, "plugin": {"type": "directory"}, "plugin/desktop": {"type": "directory"}, "plugin/desktop/sun_java.desktop": {"downloads": {"lzma": {"sha1": "49ab0ccb54c3be68281d05055bc56a88b1281d3c", "size": 447, "url": "https://launcher.mojang.com/v1/objects/49ab0ccb54c3be68281d05055bc56a88b1281d3c/sun_java.desktop"}, "raw": {"sha1": "79120ee8160ad6f3c9b90c2641fb7edf3af96b5d", "size": 624, "url": "https://launcher.mojang.com/v1/objects/79120ee8160ad6f3c9b90c2641fb7edf3af96b5d/sun_java.desktop"}}, "executable": false, "type": "file"}, "plugin/desktop/sun_java.png": {"downloads": {"raw": {"sha1": "699c41e97a35414e72a80327a54d6e14e874e951", "size": 4351, "url": "https://launcher.mojang.com/v1/objects/699c41e97a35414e72a80327a54d6e14e874e951/sun_java.png"}}, "executable": false, "type": "file"}, "release": {"downloads": {"raw": {"sha1": "cb462682644c0275d94a45b759108815f3112064", "size": 424, "url": "https://launcher.mojang.com/v1/objects/cb462682644c0275d94a45b759108815f3112064/release"}}, "executable": false, "type": "file"}}} \ No newline at end of file diff --git a/ultimmc/launcher/mojang/testdata/inspect/a/b.txt b/ultimmc/launcher/mojang/testdata/inspect/a/b.txt new file mode 100755 index 0000000..e69de29 diff --git a/ultimmc/launcher/mojang/testdata/inspect/a/b/b.txt b/ultimmc/launcher/mojang/testdata/inspect/a/b/b.txt new file mode 120000 index 0000000..4e19a04 --- /dev/null +++ b/ultimmc/launcher/mojang/testdata/inspect/a/b/b.txt @@ -0,0 +1 @@ +../b.txt \ No newline at end of file diff --git a/ultimmc/launcher/mojang/testdata/inspect_win/a/b.txt b/ultimmc/launcher/mojang/testdata/inspect_win/a/b.txt new file mode 100644 index 0000000..e69de29 diff --git a/ultimmc/launcher/mojang/testdata/inspect_win/a/b/b.txt b/ultimmc/launcher/mojang/testdata/inspect_win/a/b/b.txt new file mode 100644 index 0000000..e69de29 diff --git a/ultimmc/launcher/net/ByteArraySink.h b/ultimmc/launcher/net/ByteArraySink.h new file mode 100644 index 0000000..20e6764 --- /dev/null +++ b/ultimmc/launcher/net/ByteArraySink.h @@ -0,0 +1,62 @@ +#pragma once + +#include "Sink.h" + +namespace Net { +/* + * Sink object for downloads that uses an external QByteArray it doesn't own as a target. + */ +class ByteArraySink : public Sink +{ +public: + ByteArraySink(QByteArray *output) + :m_output(output) + { + // nil + }; + + virtual ~ByteArraySink() + { + // nil + } + +public: + JobStatus init(QNetworkRequest & request) override + { + m_output->clear(); + if(initAllValidators(request)) + return Job_InProgress; + return Job_Failed; + }; + + JobStatus write(QByteArray & data) override + { + m_output->append(data); + if(writeAllValidators(data)) + return Job_InProgress; + return Job_Failed; + } + + JobStatus abort() override + { + m_output->clear(); + failAllValidators(); + return Job_Failed; + } + + JobStatus finalize(QNetworkReply &reply) override + { + if(finalizeAllValidators(reply)) + return Job_Finished; + return Job_Failed; + } + + bool hasLocalData() override + { + return false; + } + +private: + QByteArray * m_output; +}; +} diff --git a/ultimmc/launcher/net/ChecksumValidator.h b/ultimmc/launcher/net/ChecksumValidator.h new file mode 100644 index 0000000..0d6b19c --- /dev/null +++ b/ultimmc/launcher/net/ChecksumValidator.h @@ -0,0 +1,55 @@ +#pragma once + +#include "Validator.h" +#include +#include +#include + +namespace Net { +class ChecksumValidator: public Validator +{ +public: /* con/des */ + ChecksumValidator(QCryptographicHash::Algorithm algorithm, QByteArray expected = QByteArray()) + :m_checksum(algorithm), m_expected(expected) + { + }; + virtual ~ChecksumValidator() {}; + +public: /* methods */ + bool init(QNetworkRequest &) override + { + m_checksum.reset(); + return true; + } + bool write(QByteArray & data) override + { + m_checksum.addData(data); + return true; + } + bool abort() override + { + return true; + } + bool validate(QNetworkReply &) override + { + if(m_expected.size() && m_expected != hash()) + { + qWarning() << "Checksum mismatch, download is bad."; + return false; + } + return true; + } + QByteArray hash() + { + return m_checksum.result(); + } + void setExpected(QByteArray expected) + { + m_expected = expected; + } + +private: /* data */ + QCryptographicHash m_checksum; + QByteArray m_expected; +}; +} \ No newline at end of file diff --git a/ultimmc/launcher/net/Download.cpp b/ultimmc/launcher/net/Download.cpp new file mode 100644 index 0000000..541be99 --- /dev/null +++ b/ultimmc/launcher/net/Download.cpp @@ -0,0 +1,310 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Download.h" + +#include +#include +#include + +#include "FileSystem.h" +#include "ChecksumValidator.h" +#include "MetaCacheSink.h" +#include "ByteArraySink.h" + +#include "BuildConfig.h" + +namespace Net { + +Download::Download():NetAction() +{ + m_status = Job_NotStarted; +} + +Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) +{ + Download * dl = new Download(); + dl->m_url = url; + dl->m_options = options; + auto md5Node = new ChecksumValidator(QCryptographicHash::Md5); + auto cachedNode = new MetaCacheSink(entry, md5Node); + dl->m_sink.reset(cachedNode); + dl->m_target_path = entry->getFullPath(); + return dl; +} + +Download::Ptr Download::makeByteArray(QUrl url, QByteArray *output, Options options) +{ + Download * dl = new Download(); + dl->m_url = url; + dl->m_options = options; + dl->m_sink.reset(new ByteArraySink(output)); + return dl; +} + +Download::Ptr Download::makeFile(QUrl url, QString path, Options options) +{ + Download * dl = new Download(); + dl->m_url = url; + dl->m_options = options; + dl->m_sink.reset(new FileSink(path)); + return dl; +} + +void Download::addValidator(Validator * v) +{ + m_sink->addValidator(v); +} + +void Download::startImpl() +{ + if(m_status == Job_Aborted) + { + qWarning() << "Attempt to start an aborted Download:" << m_url.toString(); + emit aborted(m_index_within_job); + return; + } + QNetworkRequest request(m_url); + m_status = m_sink->init(request); + switch(m_status) + { + case Job_Finished: + emit succeeded(m_index_within_job); + qDebug() << "Download cache hit " << m_url.toString(); + return; + case Job_InProgress: + qDebug() << "Downloading " << m_url.toString(); + break; + case Job_Failed_Proceed: // this is meaningless in this context. We do need a sink. + case Job_NotStarted: + case Job_Failed: + emit failed(m_index_within_job); + return; + case Job_Aborted: + return; + } + + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT); + + QNetworkReply *rep = m_network->get(request); + + m_reply.reset(rep); + connect(rep, SIGNAL(downloadProgress(qint64, qint64)), SLOT(downloadProgress(qint64, qint64))); + connect(rep, SIGNAL(finished()), SLOT(downloadFinished())); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, &QNetworkReply::sslErrors, this, &Download::sslErrors); + connect(rep, &QNetworkReply::readyRead, this, &Download::downloadReadyRead); +} + +void Download::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + m_total_progress = bytesTotal; + m_progress = bytesReceived; + emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); +} + +void Download::downloadError(QNetworkReply::NetworkError error) +{ + if(error == QNetworkReply::OperationCanceledError) + { + qCritical() << "Aborted " << m_url.toString(); + m_status = Job_Aborted; + } + else + { + if(m_options & Option::AcceptLocalFiles) + { + if(m_sink->hasLocalData()) + { + m_status = Job_Failed_Proceed; + return; + } + } + // error happened during download. + qCritical() << "Failed " << m_url.toString() << " with reason " << error; + m_status = Job_Failed; + } +} + +void Download::sslErrors(const QList & errors) +{ + int i = 1; + for (auto error : errors) + { + qCritical() << "Download" << m_url.toString() << "SSL Error #" << i << " : " << error.errorString(); + auto cert = error.certificate(); + qCritical() << "Certificate in question:\n" << cert.toText(); + i++; + } +} + +bool Download::handleRedirect() +{ + QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl(); + if(!redirect.isValid()) + { + if(!m_reply->hasRawHeader("Location")) + { + // no redirect -> it's fine to continue + return false; + } + // there is a Location header, but it's not correct. we need to apply some workarounds... + QByteArray redirectBA = m_reply->rawHeader("Location"); + if(redirectBA.size() == 0) + { + // empty, yet present redirect header? WTF? + return false; + } + QString redirectStr = QString::fromUtf8(redirectBA); + + if(redirectStr.startsWith("//")) + { + /* + * IF the URL begins with //, we need to insert the URL scheme. + * See: https://bugreports.qt.io/browse/QTBUG-41061 + * See: http://tools.ietf.org/html/rfc3986#section-4.2 + */ + redirectStr = m_reply->url().scheme() + ":" + redirectStr; + } + else if(redirectStr.startsWith("/")) + { + /* + * IF the URL begins with /, we need to process it as a relative URL + */ + auto url = m_reply->url(); + url.setPath(redirectStr, QUrl::TolerantMode); + redirectStr = url.toString(); + } + + /* + * Next, make sure the URL is parsed in tolerant mode. Qt doesn't parse the location header in tolerant mode, which causes issues. + * FIXME: report Qt bug for this + */ + redirect = QUrl(redirectStr, QUrl::TolerantMode); + if(!redirect.isValid()) + { + qWarning() << "Failed to parse redirect URL:" << redirectStr; + downloadError(QNetworkReply::ProtocolFailure); + return false; + } + qDebug() << "Fixed location header:" << redirect; + } + else + { + qDebug() << "Location header:" << redirect; + } + + m_url = QUrl(redirect.toString()); + qDebug() << "Following redirect to " << m_url.toString(); + start(m_network); + return true; +} + + +void Download::downloadFinished() +{ + // handle HTTP redirection first + if(handleRedirect()) + { + qDebug() << "Download redirected:" << m_url.toString(); + return; + } + + // if the download failed before this point ... + if (m_status == Job_Failed_Proceed) + { + qDebug() << "Download failed but we are allowed to proceed:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit succeeded(m_index_within_job); + return; + } + else if (m_status == Job_Failed) + { + qDebug() << "Download failed in previous step:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit failed(m_index_within_job); + return; + } + else if(m_status == Job_Aborted) + { + qDebug() << "Download aborted in previous step:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit aborted(m_index_within_job); + return; + } + + // make sure we got all the remaining data, if any + auto data = m_reply->readAll(); + if(data.size()) + { + qDebug() << "Writing extra" << data.size() << "bytes to" << m_target_path; + m_status = m_sink->write(data); + } + + // otherwise, finalize the whole graph + m_status = m_sink->finalize(*m_reply.get()); + if (m_status != Job_Finished) + { + qDebug() << "Download failed to finalize:" << m_url.toString(); + m_sink->abort(); + m_reply.reset(); + emit failed(m_index_within_job); + return; + } + m_reply.reset(); + qDebug() << "Download succeeded:" << m_url.toString(); + emit succeeded(m_index_within_job); +} + +void Download::downloadReadyRead() +{ + if(m_status == Job_InProgress) + { + auto data = m_reply->readAll(); + m_status = m_sink->write(data); + if(m_status == Job_Failed) + { + qCritical() << "Failed to process response chunk for " << m_target_path; + } + // qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes"; + } + else + { + qCritical() << "Cannot write to " << m_target_path << ", illegal status" << m_status; + } +} + +} + +bool Net::Download::abort() +{ + if(m_reply) + { + m_reply->abort(); + } + else + { + m_status = Job_Aborted; + } + return true; +} + +bool Net::Download::canAbort() +{ + return true; +} diff --git a/ultimmc/launcher/net/Download.h b/ultimmc/launcher/net/Download.h new file mode 100644 index 0000000..4478dc3 --- /dev/null +++ b/ultimmc/launcher/net/Download.h @@ -0,0 +1,77 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "NetAction.h" +#include "HttpMetaCache.h" +#include "Validator.h" +#include "Sink.h" + +#include "QObjectPtr.h" + +namespace Net { +class Download : public NetAction +{ + Q_OBJECT + +public: /* types */ + typedef shared_qobject_ptr Ptr; + enum class Option + { + NoOptions = 0, + AcceptLocalFiles = 1 + }; + Q_DECLARE_FLAGS(Options, Option) + +protected: /* con/des */ + explicit Download(); +public: + virtual ~Download(){}; + static Download::Ptr makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions); + static Download::Ptr makeByteArray(QUrl url, QByteArray *output, Options options = Option::NoOptions); + static Download::Ptr makeFile(QUrl url, QString path, Options options = Option::NoOptions); + +public: /* methods */ + QString getTargetFilepath() + { + return m_target_path; + } + void addValidator(Validator * v); + bool abort() override; + bool canAbort() override; + +private: /* methods */ + bool handleRedirect(); + +protected slots: + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; + void downloadError(QNetworkReply::NetworkError error) override; + void sslErrors(const QList & errors); + void downloadFinished() override; + void downloadReadyRead() override; + +public slots: + void startImpl() override; + +private: /* data */ + // FIXME: remove this, it has no business being here. + QString m_target_path; + std::unique_ptr m_sink; + Options m_options; +}; +} + +Q_DECLARE_OPERATORS_FOR_FLAGS(Net::Download::Options) diff --git a/ultimmc/launcher/net/FileSink.cpp b/ultimmc/launcher/net/FileSink.cpp new file mode 100644 index 0000000..7e9b892 --- /dev/null +++ b/ultimmc/launcher/net/FileSink.cpp @@ -0,0 +1,114 @@ +#include "FileSink.h" +#include +#include +#include "FileSystem.h" + +namespace Net { + +FileSink::FileSink(QString filename) + :m_filename(filename) +{ + // nil +} + +FileSink::~FileSink() +{ + // nil +} + +JobStatus FileSink::init(QNetworkRequest& request) +{ + auto result = initCache(request); + if(result != Job_InProgress) + { + return result; + } + // create a new save file and open it for writing + if (!FS::ensureFilePathExists(m_filename)) + { + qCritical() << "Could not create folder for " + m_filename; + return Job_Failed; + } + wroteAnyData = false; + m_output_file.reset(new QSaveFile(m_filename)); + if (!m_output_file->open(QIODevice::WriteOnly)) + { + qCritical() << "Could not open " + m_filename + " for writing"; + return Job_Failed; + } + + if(initAllValidators(request)) + return Job_InProgress; + return Job_Failed; +} + +JobStatus FileSink::initCache(QNetworkRequest &) +{ + return Job_InProgress; +} + +JobStatus FileSink::write(QByteArray& data) +{ + if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) + { + qCritical() << "Failed writing into " + m_filename; + m_output_file->cancelWriting(); + m_output_file.reset(); + wroteAnyData = false; + return Job_Failed; + } + wroteAnyData = true; + return Job_InProgress; +} + +JobStatus FileSink::abort() +{ + m_output_file->cancelWriting(); + failAllValidators(); + return Job_Failed; +} + +JobStatus FileSink::finalize(QNetworkReply& reply) +{ + bool gotFile = false; + QVariant statusCodeV = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute); + bool validStatus = false; + int statusCode = statusCodeV.toInt(&validStatus); + if(validStatus) + { + // this leaves out 304 Not Modified + gotFile = statusCode == 200 || statusCode == 203; + } + // if we wrote any data to the save file, we try to commit the data to the real file. + // if it actually got a proper file, we write it even if it was empty + if (gotFile || wroteAnyData) + { + // ask validators for data consistency + // we only do this for actual downloads, not 'your data is still the same' cache hits + if(!finalizeAllValidators(reply)) + return Job_Failed; + // nothing went wrong... + if (!m_output_file->commit()) + { + qCritical() << "Failed to commit changes to " << m_filename; + m_output_file->cancelWriting(); + return Job_Failed; + } + } + // then get rid of the save file + m_output_file.reset(); + + return finalizeCache(reply); +} + +JobStatus FileSink::finalizeCache(QNetworkReply &) +{ + return Job_Finished; +} + +bool FileSink::hasLocalData() +{ + QFileInfo info(m_filename); + return info.exists() && info.size() != 0; +} +} diff --git a/ultimmc/launcher/net/FileSink.h b/ultimmc/launcher/net/FileSink.h new file mode 100644 index 0000000..875fe51 --- /dev/null +++ b/ultimmc/launcher/net/FileSink.h @@ -0,0 +1,28 @@ +#pragma once +#include "Sink.h" +#include + +namespace Net { +class FileSink : public Sink +{ +public: /* con/des */ + FileSink(QString filename); + virtual ~FileSink(); + +public: /* methods */ + JobStatus init(QNetworkRequest & request) override; + JobStatus write(QByteArray & data) override; + JobStatus abort() override; + JobStatus finalize(QNetworkReply & reply) override; + bool hasLocalData() override; + +protected: /* methods */ + virtual JobStatus initCache(QNetworkRequest &); + virtual JobStatus finalizeCache(QNetworkReply &reply); + +protected: /* data */ + QString m_filename; + bool wroteAnyData = false; + std::unique_ptr m_output_file; +}; +} diff --git a/ultimmc/launcher/net/HttpMetaCache.cpp b/ultimmc/launcher/net/HttpMetaCache.cpp new file mode 100644 index 0000000..8734e0b --- /dev/null +++ b/ultimmc/launcher/net/HttpMetaCache.cpp @@ -0,0 +1,272 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "HttpMetaCache.h" +#include "FileSystem.h" + +#include +#include +#include +#include + +#include + +#include +#include +#include + +QString MetaEntry::getFullPath() +{ + // FIXME: make local? + return FS::PathCombine(basePath, relativePath); +} + +HttpMetaCache::HttpMetaCache(QString path) : QObject() +{ + m_index_file = path; + saveBatchingTimer.setSingleShot(true); + saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer); + connect(&saveBatchingTimer, SIGNAL(timeout()), SLOT(SaveNow())); +} + +HttpMetaCache::~HttpMetaCache() +{ + saveBatchingTimer.stop(); + SaveNow(); +} + +MetaEntryPtr HttpMetaCache::getEntry(QString base, QString resource_path) +{ + // no base. no base path. can't store + if (!m_entries.contains(base)) + { + // TODO: log problem + return MetaEntryPtr(); + } + EntryMap &map = m_entries[base]; + if (map.entry_list.contains(resource_path)) + { + return map.entry_list[resource_path]; + } + return MetaEntryPtr(); +} + +MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) +{ + auto entry = getEntry(base, resource_path); + // it's not present? generate a default stale entry + if (!entry) + { + return staleEntry(base, resource_path); + } + + auto &selected_base = m_entries[base]; + QString real_path = FS::PathCombine(selected_base.base_path, resource_path); + QFileInfo finfo(real_path); + + // is the file really there? if not -> stale + if (!finfo.isFile() || !finfo.isReadable()) + { + // if the file doesn't exist, we disown the entry + selected_base.entry_list.remove(resource_path); + return staleEntry(base, resource_path); + } + + if (!expected_etag.isEmpty() && expected_etag != entry->etag) + { + // if the etag doesn't match expected, we disown the entry + selected_base.entry_list.remove(resource_path); + return staleEntry(base, resource_path); + } + + // if the file changed, check md5sum + qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch(); + if (file_last_changed != entry->local_changed_timestamp) + { + QFile input(real_path); + input.open(QIODevice::ReadOnly); + QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5) + .toHex() + .constData(); + if (entry->md5sum != md5sum) + { + selected_base.entry_list.remove(resource_path); + return staleEntry(base, resource_path); + } + // md5sums matched... keep entry and save the new state to file + entry->local_changed_timestamp = file_last_changed; + SaveEventually(); + } + + // entry passed all the checks we cared about. + entry->basePath = getBasePath(base); + return entry; +} + +bool HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) +{ + if (!m_entries.contains(stale_entry->baseId)) + { + qCritical() << "Cannot add entry with unknown base: " + << stale_entry->baseId.toLocal8Bit(); + return false; + } + if (stale_entry->stale) + { + qCritical() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit(); + return false; + } + m_entries[stale_entry->baseId].entry_list[stale_entry->relativePath] = stale_entry; + SaveEventually(); + return true; +} + +bool HttpMetaCache::evictEntry(MetaEntryPtr entry) +{ + if(entry) + { + entry->stale = true; + SaveEventually(); + return true; + } + return false; +} + +MetaEntryPtr HttpMetaCache::staleEntry(QString base, QString resource_path) +{ + auto foo = new MetaEntry(); + foo->baseId = base; + foo->basePath = getBasePath(base); + foo->relativePath = resource_path; + foo->stale = true; + return MetaEntryPtr(foo); +} + +void HttpMetaCache::addBase(QString base, QString base_root) +{ + // TODO: report error + if (m_entries.contains(base)) + return; + // TODO: check if the base path is valid + EntryMap foo; + foo.base_path = base_root; + m_entries[base] = foo; +} + +QString HttpMetaCache::getBasePath(QString base) +{ + if (m_entries.contains(base)) + { + return m_entries[base].base_path; + } + return QString(); +} + +void HttpMetaCache::Load() +{ + if(m_index_file.isNull()) + return; + + QFile index(m_index_file); + if (!index.open(QIODevice::ReadOnly)) + return; + + QJsonDocument json = QJsonDocument::fromJson(index.readAll()); + if (!json.isObject()) + return; + auto root = json.object(); + // check file version first + auto version_val = root.value("version"); + if (!version_val.isString()) + return; + if (version_val.toString() != "1") + return; + + // read the entry array + auto entries_val = root.value("entries"); + if (!entries_val.isArray()) + return; + QJsonArray array = entries_val.toArray(); + for (auto element : array) + { + if (!element.isObject()) + return; + auto element_obj = element.toObject(); + QString base = element_obj.value("base").toString(); + if (!m_entries.contains(base)) + continue; + auto &entrymap = m_entries[base]; + auto foo = new MetaEntry(); + foo->baseId = base; + QString path = foo->relativePath = element_obj.value("path").toString(); + foo->md5sum = element_obj.value("md5sum").toString(); + foo->etag = element_obj.value("etag").toString(); + foo->local_changed_timestamp = element_obj.value("last_changed_timestamp").toDouble(); + foo->remote_changed_timestamp = + element_obj.value("remote_changed_timestamp").toString(); + // presumed innocent until closer examination + foo->stale = false; + entrymap.entry_list[path] = MetaEntryPtr(foo); + } +} + +void HttpMetaCache::SaveEventually() +{ + // reset the save timer + saveBatchingTimer.stop(); + saveBatchingTimer.start(30000); +} + +void HttpMetaCache::SaveNow() +{ + if(m_index_file.isNull()) + return; + QJsonObject toplevel; + toplevel.insert("version", QJsonValue(QString("1"))); + QJsonArray entriesArr; + for (auto group : m_entries) + { + for (auto entry : group.entry_list) + { + // do not save stale entries. they are dead. + if(entry->stale) + { + continue; + } + QJsonObject entryObj; + entryObj.insert("base", QJsonValue(entry->baseId)); + entryObj.insert("path", QJsonValue(entry->relativePath)); + entryObj.insert("md5sum", QJsonValue(entry->md5sum)); + entryObj.insert("etag", QJsonValue(entry->etag)); + entryObj.insert("last_changed_timestamp", + QJsonValue(double(entry->local_changed_timestamp))); + if (!entry->remote_changed_timestamp.isEmpty()) + entryObj.insert("remote_changed_timestamp", + QJsonValue(entry->remote_changed_timestamp)); + entriesArr.append(entryObj); + } + } + toplevel.insert("entries", entriesArr); + + QJsonDocument doc(toplevel); + try + { + FS::write(m_index_file, doc.toJson()); + } + catch (const Exception &e) + { + qWarning() << e.what(); + } +} diff --git a/ultimmc/launcher/net/HttpMetaCache.h b/ultimmc/launcher/net/HttpMetaCache.h new file mode 100644 index 0000000..1c10e8c --- /dev/null +++ b/ultimmc/launcher/net/HttpMetaCache.h @@ -0,0 +1,123 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include +#include +#include + +class HttpMetaCache; + +class MetaEntry +{ +friend class HttpMetaCache; +protected: + MetaEntry() {} +public: + bool isStale() + { + return stale; + } + void setStale(bool stale) + { + this->stale = stale; + } + QString getFullPath(); + QString getRemoteChangedTimestamp() + { + return remote_changed_timestamp; + } + void setRemoteChangedTimestamp(QString remote_changed_timestamp) + { + this->remote_changed_timestamp = remote_changed_timestamp; + } + void setLocalChangedTimestamp(qint64 timestamp) + { + local_changed_timestamp = timestamp; + } + QString getETag() + { + return etag; + } + void setETag(QString etag) + { + this->etag = etag; + } + QString getMD5Sum() + { + return md5sum; + } + void setMD5Sum(QString md5sum) + { + this->md5sum = md5sum; + } +protected: + QString baseId; + QString basePath; + QString relativePath; + QString md5sum; + QString etag; + qint64 local_changed_timestamp = 0; + QString remote_changed_timestamp; // QString for now, RFC 2822 encoded time + bool stale = true; +}; + +typedef std::shared_ptr MetaEntryPtr; + +class HttpMetaCache : public QObject +{ + Q_OBJECT +public: + // supply path to the cache index file + HttpMetaCache(QString path = QString()); + ~HttpMetaCache(); + + // get the entry solely from the cache + // you probably don't want this, unless you have some specific caching needs. + MetaEntryPtr getEntry(QString base, QString resource_path); + + // get the entry from cache and verify that it isn't stale (within reason) + MetaEntryPtr resolveEntry(QString base, QString resource_path, + QString expected_etag = QString()); + + // add a previously resolved stale entry + bool updateEntry(MetaEntryPtr stale_entry); + + // evict selected entry from cache + bool evictEntry(MetaEntryPtr entry); + + void addBase(QString base, QString base_root); + + // (re)start a timer that calls SaveNow later. + void SaveEventually(); + void Load(); + QString getBasePath(QString base); +public +slots: + void SaveNow(); + +private: + // create a new stale entry, given the parameters + MetaEntryPtr staleEntry(QString base, QString resource_path); + struct EntryMap + { + QString base_path; + QMap entry_list; + }; + QMap m_entries; + QString m_index_file; + QTimer saveBatchingTimer; +}; diff --git a/ultimmc/launcher/net/MetaCacheSink.cpp b/ultimmc/launcher/net/MetaCacheSink.cpp new file mode 100644 index 0000000..5cdf046 --- /dev/null +++ b/ultimmc/launcher/net/MetaCacheSink.cpp @@ -0,0 +1,65 @@ +#include "MetaCacheSink.h" +#include +#include +#include "FileSystem.h" +#include "Application.h" + +namespace Net { + +MetaCacheSink::MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum) + :Net::FileSink(entry->getFullPath()), m_entry(entry), m_md5Node(md5sum) +{ + addValidator(md5sum); +} + +MetaCacheSink::~MetaCacheSink() +{ + // nil +} + +JobStatus MetaCacheSink::initCache(QNetworkRequest& request) +{ + if (!m_entry->isStale()) + { + return Job_Finished; + } + // check if file exists, if it does, use its information for the request + QFile current(m_filename); + if(current.exists() && current.size() != 0) + { + if (m_entry->getRemoteChangedTimestamp().size()) + { + request.setRawHeader(QString("If-Modified-Since").toLatin1(), m_entry->getRemoteChangedTimestamp().toLatin1()); + } + if (m_entry->getETag().size()) + { + request.setRawHeader(QString("If-None-Match").toLatin1(), m_entry->getETag().toLatin1()); + } + } + return Job_InProgress; +} + +JobStatus MetaCacheSink::finalizeCache(QNetworkReply & reply) +{ + QFileInfo output_file_info(m_filename); + if(wroteAnyData) + { + m_entry->setMD5Sum(m_md5Node->hash().toHex().constData()); + } + m_entry->setETag(reply.rawHeader("ETag").constData()); + if (reply.hasRawHeader("Last-Modified")) + { + m_entry->setRemoteChangedTimestamp(reply.rawHeader("Last-Modified").constData()); + } + m_entry->setLocalChangedTimestamp(output_file_info.lastModified().toUTC().toMSecsSinceEpoch()); + m_entry->setStale(false); + APPLICATION->metacache()->updateEntry(m_entry); + return Job_Finished; +} + +bool MetaCacheSink::hasLocalData() +{ + QFileInfo info(m_filename); + return info.exists() && info.size() != 0; +} +} diff --git a/ultimmc/launcher/net/MetaCacheSink.h b/ultimmc/launcher/net/MetaCacheSink.h new file mode 100644 index 0000000..edcf7ad --- /dev/null +++ b/ultimmc/launcher/net/MetaCacheSink.h @@ -0,0 +1,22 @@ +#pragma once +#include "FileSink.h" +#include "ChecksumValidator.h" +#include "net/HttpMetaCache.h" + +namespace Net { +class MetaCacheSink : public FileSink +{ +public: /* con/des */ + MetaCacheSink(MetaEntryPtr entry, ChecksumValidator * md5sum); + virtual ~MetaCacheSink(); + bool hasLocalData() override; + +protected: /* methods */ + JobStatus initCache(QNetworkRequest & request) override; + JobStatus finalizeCache(QNetworkReply & reply) override; + +private: /* data */ + MetaEntryPtr m_entry; + ChecksumValidator * m_md5Node; +}; +} diff --git a/ultimmc/launcher/net/Mode.h b/ultimmc/launcher/net/Mode.h new file mode 100644 index 0000000..9a95f5a --- /dev/null +++ b/ultimmc/launcher/net/Mode.h @@ -0,0 +1,10 @@ +#pragma once + +namespace Net +{ +enum class Mode +{ + Offline, + Online +}; +} diff --git a/ultimmc/launcher/net/NetAction.h b/ultimmc/launcher/net/NetAction.h new file mode 100644 index 0000000..efb2095 --- /dev/null +++ b/ultimmc/launcher/net/NetAction.h @@ -0,0 +1,122 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include + +enum JobStatus +{ + Job_NotStarted, + Job_InProgress, + Job_Finished, + Job_Failed, + Job_Aborted, + /* + * FIXME: @NUKE this confuses the task failing with us having a fallback in the form of local data. Clear up the confusion. + * Same could be true for aborted task - the presence of pre-existing result is a separate concern + */ + Job_Failed_Proceed +}; + +class NetAction : public QObject +{ + Q_OBJECT +protected: + explicit NetAction() : QObject(nullptr) {}; + +public: + using Ptr = shared_qobject_ptr; + + virtual ~NetAction() {}; + + bool isRunning() const + { + return m_status == Job_InProgress; + } + bool isFinished() const + { + return m_status >= Job_Finished; + } + bool wasSuccessful() const + { + return m_status == Job_Finished || m_status == Job_Failed_Proceed; + } + + qint64 totalProgress() const + { + return m_total_progress; + } + qint64 currentProgress() const + { + return m_progress; + } + virtual bool abort() + { + return false; + } + virtual bool canAbort() + { + return false; + } + QUrl url() + { + return m_url; + } + +signals: + void started(int index); + void netActionProgress(int index, qint64 current, qint64 total); + void succeeded(int index); + void failed(int index); + void aborted(int index); + +protected slots: + virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) = 0; + virtual void downloadError(QNetworkReply::NetworkError error) = 0; + virtual void downloadFinished() = 0; + virtual void downloadReadyRead() = 0; + +public slots: + void start(shared_qobject_ptr network) { + m_network = network; + startImpl(); + } + +protected: + virtual void startImpl() = 0; + +public: + shared_qobject_ptr m_network; + + /// index within the parent job, FIXME: nuke + int m_index_within_job = 0; + + /// the network reply + unique_qobject_ptr m_reply; + + /// source URL + QUrl m_url; + + qint64 m_progress = 0; + qint64 m_total_progress = 1; + +protected: + JobStatus m_status = Job_NotStarted; +}; diff --git a/ultimmc/launcher/net/NetJob.cpp b/ultimmc/launcher/net/NetJob.cpp new file mode 100644 index 0000000..9bad89e --- /dev/null +++ b/ultimmc/launcher/net/NetJob.cpp @@ -0,0 +1,218 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "NetJob.h" +#include "Download.h" + +#include + +void NetJob::partSucceeded(int index) +{ + // do progress. all slots are 1 in size at least + auto &slot = parts_progress[index]; + partProgress(index, slot.total_progress, slot.total_progress); + + m_doing.remove(index); + m_done.insert(index); + downloads[index].get()->disconnect(this); + startMoreParts(); +} + +void NetJob::partFailed(int index) +{ + m_doing.remove(index); + auto &slot = parts_progress[index]; + if (slot.failures == 3) + { + m_failed.insert(index); + } + else + { + slot.failures++; + m_todo.enqueue(index); + } + downloads[index].get()->disconnect(this); + startMoreParts(); +} + +void NetJob::partAborted(int index) +{ + m_aborted = true; + m_doing.remove(index); + m_failed.insert(index); + downloads[index].get()->disconnect(this); + startMoreParts(); +} + +void NetJob::partProgress(int index, qint64 bytesReceived, qint64 bytesTotal) +{ + auto &slot = parts_progress[index]; + slot.current_progress = bytesReceived; + slot.total_progress = bytesTotal; + + int done = m_done.size(); + int doing = m_doing.size(); + int all = parts_progress.size(); + + qint64 bytesAll = 0; + qint64 bytesTotalAll = 0; + for(auto & partIdx: m_doing) + { + auto part = parts_progress[partIdx]; + // do not count parts with unknown/nonsensical total size + if(part.total_progress <= 0) + { + continue; + } + bytesAll += part.current_progress; + bytesTotalAll += part.total_progress; + } + + qint64 inprogress = (bytesTotalAll == 0) ? 0 : (bytesAll * 1000) / bytesTotalAll; + auto current = done * 1000 + doing * inprogress; + auto current_total = all * 1000; + // HACK: make sure it never jumps backwards. + // FAIL: This breaks if the size is not known (or is it something else?) and jumps to 1000, so if it is 1000 reset it to inprogress + if(m_current_progress == 1000) { + m_current_progress = inprogress; + } + if(m_current_progress > current) + { + current = m_current_progress; + } + m_current_progress = current; + setProgress(current, current_total); +} + +void NetJob::executeTask() +{ + // hack that delays early failures so they can be caught easier + QMetaObject::invokeMethod(this, "startMoreParts", Qt::QueuedConnection); +} + +void NetJob::startMoreParts() +{ + if(!isRunning()) + { + // this actually makes sense. You can put running downloads into a NetJob and then not start it until much later. + return; + } + // OK. We are actively processing tasks, proceed. + // Check for final conditions if there's nothing in the queue. + if(!m_todo.size()) + { + if(!m_doing.size()) + { + if(!m_failed.size()) + { + emitSucceeded(); + } + else if(m_aborted) + { + emitAborted(); + } + else + { + emitFailed(tr("Job '%1' failed to process:\n%2").arg(objectName()).arg(getFailedFiles().join("\n"))); + } + } + return; + } + // There's work to do, try to start more parts. + while (m_doing.size() < 6) + { + if(!m_todo.size()) + return; + int doThis = m_todo.dequeue(); + m_doing.insert(doThis); + auto part = downloads[doThis]; + // connect signals :D + connect(part.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int))); + connect(part.get(), SIGNAL(failed(int)), SLOT(partFailed(int))); + connect(part.get(), SIGNAL(aborted(int)), SLOT(partAborted(int))); + connect(part.get(), SIGNAL(netActionProgress(int, qint64, qint64)), + SLOT(partProgress(int, qint64, qint64))); + part->start(m_network); + } +} + + +QStringList NetJob::getFailedFiles() +{ + QStringList failed; + for (auto index: m_failed) + { + failed.push_back(downloads[index]->url().toString()); + } + failed.sort(); + return failed; +} + +bool NetJob::canAbort() const +{ + bool canFullyAbort = true; + // can abort the waiting? + for(auto index: m_todo) + { + auto part = downloads[index]; + canFullyAbort &= part->canAbort(); + } + // can abort the active? + for(auto index: m_doing) + { + auto part = downloads[index]; + canFullyAbort &= part->canAbort(); + } + return canFullyAbort; +} + +bool NetJob::abort() +{ + bool fullyAborted = true; + // fail all waiting + m_failed.unite(m_todo.toSet()); + m_todo.clear(); + // abort active + auto toKill = m_doing.toList(); + for(auto index: toKill) + { + auto part = downloads[index]; + fullyAborted &= part->abort(); + } + return fullyAborted; +} + +bool NetJob::addNetAction(NetAction::Ptr action) +{ + action->m_index_within_job = downloads.size(); + downloads.append(action); + part_info pi; + parts_progress.append(pi); + partProgress(parts_progress.count() - 1, action->currentProgress(), action->totalProgress()); + + if(action->isRunning()) + { + connect(action.get(), SIGNAL(succeeded(int)), SLOT(partSucceeded(int))); + connect(action.get(), SIGNAL(failed(int)), SLOT(partFailed(int))); + connect(action.get(), SIGNAL(netActionProgress(int, qint64, qint64)), SLOT(partProgress(int, qint64, qint64))); + } + else + { + m_todo.append(parts_progress.size() - 1); + } + return true; +} + +NetJob::~NetJob() = default; diff --git a/ultimmc/launcher/net/NetJob.h b/ultimmc/launcher/net/NetJob.h new file mode 100644 index 0000000..fdea710 --- /dev/null +++ b/ultimmc/launcher/net/NetJob.h @@ -0,0 +1,92 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include "NetAction.h" +#include "Download.h" +#include "HttpMetaCache.h" +#include "tasks/Task.h" +#include "QObjectPtr.h" + +class NetJob; + +class NetJob : public Task +{ + Q_OBJECT +public: + using Ptr = shared_qobject_ptr; + + explicit NetJob(QString job_name, shared_qobject_ptr network) : Task(), m_network(network) + { + setObjectName(job_name); + } + virtual ~NetJob(); + + bool addNetAction(NetAction::Ptr action); + + NetAction::Ptr operator[](int index) + { + return downloads[index]; + } + const NetAction::Ptr at(const int index) + { + return downloads.at(index); + } + NetAction::Ptr first() + { + if (downloads.size()) + return downloads[0]; + return NetAction::Ptr(); + } + int size() const + { + return downloads.size(); + } + QStringList getFailedFiles(); + + bool canAbort() const override; + +private slots: + void startMoreParts(); + +public slots: + virtual void executeTask() override; + virtual bool abort() override; + +private slots: + void partProgress(int index, qint64 bytesReceived, qint64 bytesTotal); + void partSucceeded(int index); + void partFailed(int index); + void partAborted(int index); + +private: + shared_qobject_ptr m_network; + + struct part_info + { + qint64 current_progress = 0; + qint64 total_progress = 1; + int failures = 0; + }; + QList downloads; + QList parts_progress; + QQueue m_todo; + QSet m_doing; + QSet m_done; + QSet m_failed; + qint64 m_current_progress = 0; + bool m_aborted = false; +}; diff --git a/ultimmc/launcher/net/PasteUpload.cpp b/ultimmc/launcher/net/PasteUpload.cpp new file mode 100644 index 0000000..4b69b68 --- /dev/null +++ b/ultimmc/launcher/net/PasteUpload.cpp @@ -0,0 +1,105 @@ +#include "PasteUpload.h" +#include "BuildConfig.h" +#include "Application.h" + +#include +#include +#include +#include +#include + +PasteUpload::PasteUpload(QWidget *window, QString text, QString key) : m_window(window) +{ + m_key = key; + QByteArray temp; + QJsonObject topLevelObj; + QJsonObject sectionObject; + sectionObject.insert("contents", text); + QJsonArray sectionArray; + sectionArray.append(sectionObject); + topLevelObj.insert("description", "Log Upload"); + topLevelObj.insert("sections", sectionArray); + QJsonDocument docOut; + docOut.setObject(topLevelObj); + m_jsonContent = docOut.toJson(); +} + +PasteUpload::~PasteUpload() +{ +} + +bool PasteUpload::validateText() +{ + return m_jsonContent.size() <= maxSize(); +} + +void PasteUpload::executeTask() +{ + QNetworkRequest request(QUrl("https://api.paste.ee/v1/pastes")); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + + request.setRawHeader("Content-Type", "application/json"); + request.setRawHeader("Content-Length", QByteArray::number(m_jsonContent.size())); + request.setRawHeader("X-Auth-Token", m_key.toStdString().c_str()); + + QNetworkReply *rep = APPLICATION->network()->post(request, m_jsonContent); + + m_reply = std::shared_ptr(rep); + setStatus(tr("Uploading to paste.ee")); + connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); + connect(rep, SIGNAL(finished()), this, SLOT(downloadFinished())); +} + +void PasteUpload::downloadError(QNetworkReply::NetworkError error) +{ + // error happened during download. + qCritical() << "Network error: " << error; + emitFailed(m_reply->errorString()); +} + +void PasteUpload::downloadFinished() +{ + QByteArray data = m_reply->readAll(); + // if the download succeeded + if (m_reply->error() == QNetworkReply::NetworkError::NoError) + { + m_reply.reset(); + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + emitFailed(jsonError.errorString()); + return; + } + if (!parseResult(doc)) + { + emitFailed(tr("paste.ee returned an error. Please consult the logs for more information")); + return; + } + } + // else the download failed + else + { + emitFailed(QString("Network error: %1").arg(m_reply->errorString())); + m_reply.reset(); + return; + } + emitSucceeded(); +} + +bool PasteUpload::parseResult(QJsonDocument doc) +{ + auto object = doc.object(); + auto status = object.value("success").toBool(); + if (!status) + { + qCritical() << "paste.ee reported error:" << QString(object.value("error").toString()); + return false; + } + m_pasteLink = object.value("link").toString(); + m_pasteID = object.value("id").toString(); + qDebug() << m_pasteLink; + return true; +} + diff --git a/ultimmc/launcher/net/PasteUpload.h b/ultimmc/launcher/net/PasteUpload.h new file mode 100644 index 0000000..5514e05 --- /dev/null +++ b/ultimmc/launcher/net/PasteUpload.h @@ -0,0 +1,47 @@ +#pragma once +#include "tasks/Task.h" +#include +#include +#include + +class PasteUpload : public Task +{ + Q_OBJECT +public: + PasteUpload(QWidget *window, QString text, QString key = "public"); + virtual ~PasteUpload(); + + QString pasteLink() + { + return m_pasteLink; + } + QString pasteID() + { + return m_pasteID; + } + int maxSize() + { + // 2MB for paste.ee - public + if(m_key == "public") + return 1024*1024*2; + // 12MB for paste.ee - with actual key + return 1024*1024*12; + } + bool validateText(); +protected: + virtual void executeTask(); + +private: + bool parseResult(QJsonDocument doc); + QString m_error; + QWidget *m_window; + QString m_pasteID; + QString m_pasteLink; + QString m_key; + QByteArray m_jsonContent; + std::shared_ptr m_reply; +public +slots: + void downloadError(QNetworkReply::NetworkError); + void downloadFinished(); +}; diff --git a/ultimmc/launcher/net/Sink.h b/ultimmc/launcher/net/Sink.h new file mode 100644 index 0000000..d367fb1 --- /dev/null +++ b/ultimmc/launcher/net/Sink.h @@ -0,0 +1,70 @@ +#pragma once + +#include "net/NetAction.h" + +#include "Validator.h" + +namespace Net { +class Sink +{ +public: /* con/des */ + Sink() {}; + virtual ~Sink() {}; + +public: /* methods */ + virtual JobStatus init(QNetworkRequest & request) = 0; + virtual JobStatus write(QByteArray & data) = 0; + virtual JobStatus abort() = 0; + virtual JobStatus finalize(QNetworkReply & reply) = 0; + virtual bool hasLocalData() = 0; + + void addValidator(Validator * validator) + { + if(validator) + { + validators.push_back(std::shared_ptr(validator)); + } + } + +protected: /* methods */ + bool finalizeAllValidators(QNetworkReply & reply) + { + for(auto & validator: validators) + { + if(!validator->validate(reply)) + return false; + } + return true; + } + bool failAllValidators() + { + bool success = true; + for(auto & validator: validators) + { + success &= validator->abort(); + } + return success; + } + bool initAllValidators(QNetworkRequest & request) + { + for(auto & validator: validators) + { + if(!validator->init(request)) + return false; + } + return true; + } + bool writeAllValidators(QByteArray & data) + { + for(auto & validator: validators) + { + if(!validator->write(data)) + return false; + } + return true; + } + +protected: /* data */ + std::vector> validators; +}; +} diff --git a/ultimmc/launcher/net/Validator.h b/ultimmc/launcher/net/Validator.h new file mode 100644 index 0000000..59b72a0 --- /dev/null +++ b/ultimmc/launcher/net/Validator.h @@ -0,0 +1,18 @@ +#pragma once + +#include "net/NetAction.h" + +namespace Net { +class Validator +{ +public: /* con/des */ + Validator() {}; + virtual ~Validator() {}; + +public: /* methods */ + virtual bool init(QNetworkRequest & request) = 0; + virtual bool write(QByteArray & data) = 0; + virtual bool abort() = 0; + virtual bool validate(QNetworkReply & reply) = 0; +}; +} diff --git a/ultimmc/launcher/news/NewsChecker.cpp b/ultimmc/launcher/news/NewsChecker.cpp new file mode 100644 index 0000000..4f4359b --- /dev/null +++ b/ultimmc/launcher/news/NewsChecker.cpp @@ -0,0 +1,132 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "NewsChecker.h" + +#include +#include + +#include + +NewsChecker::NewsChecker(shared_qobject_ptr network, const QString& feedUrl) +{ + m_network = network; + m_feedUrl = feedUrl; +} + +void NewsChecker::reloadNews() +{ + // Start a netjob to download the RSS feed and call rssDownloadFinished() when it's done. + if (isLoadingNews()) + { + qDebug() << "Ignored request to reload news. Currently reloading already."; + return; + } + + qDebug() << "Reloading news."; + + NetJob* job = new NetJob("News RSS Feed", m_network); + job->addNetAction(Net::Download::makeByteArray(m_feedUrl, &newsData)); + QObject::connect(job, &NetJob::succeeded, this, &NewsChecker::rssDownloadFinished); + QObject::connect(job, &NetJob::failed, this, &NewsChecker::rssDownloadFailed); + m_newsNetJob.reset(job); + job->start(); +} + +void NewsChecker::rssDownloadFinished() +{ + // Parse the XML file and process the RSS feed entries. + qDebug() << "Finished loading RSS feed."; + + m_newsNetJob.reset(); + QDomDocument doc; + { + // Stuff to store error info in. + QString errorMsg = "Unknown error."; + int errorLine = -1; + int errorCol = -1; + + // Parse the XML. + if (!doc.setContent(newsData, false, &errorMsg, &errorLine, &errorCol)) + { + QString fullErrorMsg = QString("Error parsing RSS feed XML. %s at %d:%d.").arg(errorMsg, errorLine, errorCol); + fail(fullErrorMsg); + newsData.clear(); + return; + } + newsData.clear(); + } + + // If the parsing succeeded, read it. + QDomNodeList items = doc.elementsByTagName("item"); + m_newsEntries.clear(); + for (int i = 0; i < items.length(); i++) + { + QDomElement element = items.at(i).toElement(); + NewsEntryPtr entry; + entry.reset(new NewsEntry()); + QString errorMsg = "An unknown error occurred."; + if (NewsEntry::fromXmlElement(element, entry.get(), &errorMsg)) + { + qDebug() << "Loaded news entry" << entry->title; + m_newsEntries.append(entry); + } + else + { + qWarning() << "Failed to load news entry at index" << i << ":" << errorMsg; + } + } + + succeed(); +} + +void NewsChecker::rssDownloadFailed(QString reason) +{ + // Set an error message and fail. + fail(tr("Failed to load news RSS feed:\n%1").arg(reason)); +} + + +QList NewsChecker::getNewsEntries() const +{ + return m_newsEntries; +} + +bool NewsChecker::isLoadingNews() const +{ + return m_newsNetJob.get() != nullptr; +} + +QString NewsChecker::getLastLoadErrorMsg() const +{ + return m_lastLoadError; +} + +void NewsChecker::succeed() +{ + m_lastLoadError = ""; + qDebug() << "News loading succeeded."; + m_newsNetJob.reset(); + emit newsLoaded(); +} + +void NewsChecker::fail(const QString& errorMsg) +{ + m_lastLoadError = errorMsg; + qDebug() << "Failed to load news:" << errorMsg; + m_newsNetJob.reset(); + emit newsLoadingFailed(errorMsg); +} + diff --git a/ultimmc/launcher/news/NewsChecker.h b/ultimmc/launcher/news/NewsChecker.h new file mode 100644 index 0000000..8467a54 --- /dev/null +++ b/ultimmc/launcher/news/NewsChecker.h @@ -0,0 +1,105 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include + +#include "NewsEntry.h" + +class NewsChecker : public QObject +{ + Q_OBJECT +public: + /*! + * Constructs a news reader to read from the given RSS feed URL. + */ + NewsChecker(shared_qobject_ptr network, const QString& feedUrl); + + /*! + * Returns the error message for the last time the news was loaded. + * Empty string if the last load was successful. + */ + QString getLastLoadErrorMsg() const; + + /*! + * Returns true if the news has been loaded successfully. + */ + bool isNewsLoaded() const; + + //! True if the news is currently loading. If true, reloadNews() will do nothing. + bool isLoadingNews() const; + + /*! + * Returns a list of news entries. + */ + QList getNewsEntries() const; + + /*! + * Reloads the news from the website's RSS feed. + * If the news is already loading, this does nothing. + */ + void Q_SLOT reloadNews(); + +signals: + /*! + * Signal fired after the news has finished loading. + */ + void newsLoaded(); + + /*! + * Signal fired after the news fails to load. + */ + void newsLoadingFailed(QString errorMsg); + +protected slots: + void rssDownloadFinished(); + void rssDownloadFailed(QString reason); + +protected: /* data */ + //! The URL for the RSS feed to fetch. + QString m_feedUrl; + + //! List of news entries. + QList m_newsEntries; + + //! The network job to use to load the news. + NetJob::Ptr m_newsNetJob; + + //! True if news has been loaded. + bool m_loadedNews; + + QByteArray newsData; + + /*! + * Gets the error message that was given last time the news was loaded. + * If the last news load succeeded, this will be an empty string. + */ + QString m_lastLoadError; + + shared_qobject_ptr m_network; + +protected slots: + /// Emits newsLoaded() and sets m_lastLoadError to empty string. + void succeed(); + + /// Emits newsLoadingFailed() and sets m_lastLoadError to the given message. + void fail(const QString& errorMsg); +}; + diff --git a/ultimmc/launcher/news/NewsEntry.cpp b/ultimmc/launcher/news/NewsEntry.cpp new file mode 100644 index 0000000..7eff657 --- /dev/null +++ b/ultimmc/launcher/news/NewsEntry.cpp @@ -0,0 +1,77 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "NewsEntry.h" + +#include +#include + +NewsEntry::NewsEntry(QObject* parent) : + QObject(parent) +{ + this->title = tr("Untitled"); + this->content = tr("No content."); + this->link = ""; + this->author = tr("Unknown Author"); + this->pubDate = QDateTime::currentDateTime(); +} + +NewsEntry::NewsEntry(const QString& title, const QString& content, const QString& link, const QString& author, const QDateTime& pubDate, QObject* parent) : + QObject(parent) +{ + this->title = title; + this->content = content; + this->link = link; + this->author = author; + this->pubDate = pubDate; +} + +/*! + * Gets the text content of the given child element as a QVariant. + */ +inline QString childValue(const QDomElement& element, const QString& childName, QString defaultVal="") +{ + QDomNodeList nodes = element.elementsByTagName(childName); + if (nodes.count() > 0) + { + QDomElement element = nodes.at(0).toElement(); + return element.text(); + } + else + { + return defaultVal; + } +} + +bool NewsEntry::fromXmlElement(const QDomElement& element, NewsEntry* entry, QString* errorMsg) +{ + QString title = childValue(element, "title", tr("Untitled")); + QString content = childValue(element, "description", tr("No content.")); + QString link = childValue(element, "link"); + QString author = childValue(element, "dc:creator", tr("Unknown Author")); + QString pubDateStr = childValue(element, "pubDate"); + + // FIXME: For now, we're just ignoring timezones. We assume that all time zones in the RSS feed are the same. + QString dateFormat("ddd, dd MMM yyyy hh:mm:ss"); + QDateTime pubDate = QDateTime::fromString(pubDateStr, dateFormat); + + entry->title = title; + entry->content = content; + entry->link = link; + entry->author = author; + entry->pubDate = pubDate; + return true; +} + diff --git a/ultimmc/launcher/news/NewsEntry.h b/ultimmc/launcher/news/NewsEntry.h new file mode 100644 index 0000000..0dbc70a --- /dev/null +++ b/ultimmc/launcher/news/NewsEntry.h @@ -0,0 +1,65 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include + +class NewsEntry : public QObject +{ + Q_OBJECT + +public: + /*! + * Constructs an empty news entry. + */ + explicit NewsEntry(QObject* parent=0); + + /*! + * Constructs a new news entry. + * Note that content may contain HTML. + */ + NewsEntry(const QString& title, const QString& content, const QString& link, const QString& author, const QDateTime& pubDate, QObject* parent=0); + + /*! + * Attempts to load information from the given XML element into the given news entry pointer. + * If this fails, the function will return false and store an error message in the errorMsg pointer. + */ + static bool fromXmlElement(const QDomElement& element, NewsEntry* entry, QString* errorMsg=0); + + + //! The post title. + QString title; + + //! The post's content. May contain HTML. + QString content; + + //! URL to the post. + QString link; + + //! The post's author. + QString author; + + //! The date and time that this post was published. + QDateTime pubDate; +}; + +typedef std::shared_ptr NewsEntryPtr; + diff --git a/ultimmc/launcher/notifications/NotificationChecker.cpp b/ultimmc/launcher/notifications/NotificationChecker.cpp new file mode 100644 index 0000000..d91465c --- /dev/null +++ b/ultimmc/launcher/notifications/NotificationChecker.cpp @@ -0,0 +1,124 @@ +#include "NotificationChecker.h" + +#include +#include +#include +#include + +#include "net/Download.h" + +#include "Application.h" + +NotificationChecker::NotificationChecker(QObject *parent) + : QObject(parent), m_appVersionChannel("develop") +{ +} + +void NotificationChecker::setNotificationsUrl(const QUrl ¬ificationsUrl) +{ + m_notificationsUrl = notificationsUrl; +} + +void NotificationChecker::setApplicationFullVersion(QString version) +{ + m_appFullVersion = version; +} + +void NotificationChecker::setApplicationPlatform(QString platform) +{ + m_appPlatform = platform; +} + +QList NotificationChecker::notificationEntries() const +{ + return m_entries; +} + +void NotificationChecker::checkForNotifications() +{ + if (!m_notificationsUrl.isValid()) + { + qCritical() << "Failed to check for notifications. No notifications URL set." + << "If you'd like to use MultiMC's notification system, please pass the " + "URL to CMake at compile time."; + return; + } + if (m_checkJob) + { + return; + } + m_checkJob = new NetJob("Checking for notifications", APPLICATION->network()); + auto entry = APPLICATION->metacache()->resolveEntry("root", "notifications.json"); + entry->setStale(true); + m_checkJob->addNetAction(m_download = Net::Download::makeCached(m_notificationsUrl, entry)); + connect(m_download.get(), &Net::Download::succeeded, this, &NotificationChecker::downloadSucceeded); + m_checkJob->start(); +} + +void NotificationChecker::downloadSucceeded(int) +{ + m_entries.clear(); + + QFile file(m_download->getTargetFilepath()); + if (file.open(QFile::ReadOnly)) + { + QJsonArray root = QJsonDocument::fromJson(file.readAll()).array(); + for (auto it = root.begin(); it != root.end(); ++it) + { + QJsonObject obj = (*it).toObject(); + NotificationEntry entry; + entry.id = obj.value("id").toDouble(); + entry.message = obj.value("message").toString(); + entry.channel = obj.value("channel").toString(); + entry.platform = obj.value("platform").toString(); + entry.from = obj.value("from").toString(); + entry.to = obj.value("to").toString(); + const QString type = obj.value("type").toString("critical"); + if (type == "critical") + { + entry.type = NotificationEntry::Critical; + } + else if (type == "warning") + { + entry.type = NotificationEntry::Warning; + } + else if (type == "information") + { + entry.type = NotificationEntry::Information; + } + if(entryApplies(entry)) + m_entries.append(entry); + } + } + + m_checkJob.reset(); + + emit notificationCheckFinished(); +} + +bool versionLessThan(const QString &v1, const QString &v2) +{ + QStringList l1 = v1.split('.'); + QStringList l2 = v2.split('.'); + while (!l1.isEmpty() && !l2.isEmpty()) + { + int one = l1.isEmpty() ? 0 : l1.takeFirst().toInt(); + int two = l2.isEmpty() ? 0 : l2.takeFirst().toInt(); + if (one != two) + { + return one < two; + } + } + return false; +} + +bool NotificationChecker::entryApplies(const NotificationChecker::NotificationEntry& entry) const +{ + bool channelApplies = entry.channel.isEmpty() || entry.channel == m_appVersionChannel; + bool platformApplies = entry.platform.isEmpty() || entry.platform == m_appPlatform; + bool fromApplies = + entry.from.isEmpty() || entry.from == m_appFullVersion || !versionLessThan(m_appFullVersion, entry.from); + bool toApplies = + entry.to.isEmpty() || entry.to == m_appFullVersion || !versionLessThan(entry.to, m_appFullVersion); + return channelApplies && platformApplies && fromApplies && toApplies; +} diff --git a/ultimmc/launcher/notifications/NotificationChecker.h b/ultimmc/launcher/notifications/NotificationChecker.h new file mode 100644 index 0000000..4049e55 --- /dev/null +++ b/ultimmc/launcher/notifications/NotificationChecker.h @@ -0,0 +1,60 @@ +#pragma once + +#include + +#include "net/NetJob.h" +#include "net/Download.h" + +class NotificationChecker : public QObject +{ + Q_OBJECT + +public: + explicit NotificationChecker(QObject *parent = 0); + + void setNotificationsUrl(const QUrl ¬ificationsUrl); + void setApplicationPlatform(QString platform); + void setApplicationFullVersion(QString version); + + struct NotificationEntry + { + int id; + QString message; + enum + { + Critical, + Warning, + Information + } type; + QString channel; + QString platform; + QString from; + QString to; + }; + + QList notificationEntries() const; + +public +slots: + void checkForNotifications(); + +private +slots: + void downloadSucceeded(int); + +signals: + void notificationCheckFinished(); + +private: + bool entryApplies(const NotificationEntry &entry) const; + +private: + QList m_entries; + QUrl m_notificationsUrl; + NetJob::Ptr m_checkJob; + Net::Download::Ptr m_download; + + QString m_appVersionChannel; + QString m_appPlatform; + QString m_appFullVersion; +}; diff --git a/ultimmc/launcher/package/rpm/MultiMC5.spec b/ultimmc/launcher/package/rpm/MultiMC5.spec new file mode 100644 index 0000000..4b7e500 --- /dev/null +++ b/ultimmc/launcher/package/rpm/MultiMC5.spec @@ -0,0 +1,65 @@ +Name: MultiMC5 +Version: 1.4 +Release: 4%{?dist} +Summary: A local install wrapper for MultiMC + +License: ASL 2.0 +URL: https://multimc.org +ExclusiveArch: %{ix86} x86_64 + +BuildRequires: desktop-file-utils +BuildRequires: libappstream-glib +Requires: zenity %{?suse_version:lib}qt5-qtbase wget xrandr +Provides: multimc = %{version} +Provides: MultiMC = %{version} +Provides: multimc5 = %{version} + + + +%description +A local install wrapper for MultiMC + +%prep + + +%build + +%install +mkdir -p %{buildroot}/opt/multimc +install -m 0644 ../ubuntu/multimc/opt/multimc/icon.svg %{buildroot}/opt/multimc/icon.svg +install -m 0755 ../ubuntu/multimc/opt/multimc/run.sh %{buildroot}/opt/multimc/run.sh +mkdir -p %{buildroot}/%{_datadir}/applications +desktop-file-install --dir=%{buildroot}%{_datadir}/applications ../ubuntu/multimc/usr/share/applications/multimc.desktop + +mkdir -p %{buildroot}/%{_datadir}/metainfo +install -m 0644 ../ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml %{buildroot}/%{_metainfodir}/multimc.metainfo.xml +mkdir -p %{buildroot}/%{_mandir}/man1 +install -m 0644 ../ubuntu/multimc/usr/share/man/man1/multimc.1 %{buildroot}/%{_mandir}/man1/multimc.1 + +%check +appstream-util validate-relax --nonet %{buildroot}%{_metainfodir}/multimc.metainfo.xml + +%files +%dir /opt/multimc +/opt/multimc/icon.svg +/opt/multimc/run.sh +%{_datadir}/applications/multimc.desktop +%{_metainfodir}/multimc.metainfo.xml +%dir %{_mandir}/man1 +%{_mandir}/man1/multimc.1* + +%changelog +* Fri Jan 28 2022 Jan Drögehoff - 1.4-4 +- Update spec to support OpenSuse and conform to Fedora guidelines + +* Sun Oct 03 2021 imperatorstorm <30777770+ImperatorStorm@users.noreply.github.com> +- added manpage + +* Tue Jun 01 2021 kb1000 - 1.4-2 +- Add xrandr to the dependencies + +* Tue Dec 08 00:34:35 CET 2020 joshua-stone +- Add metainfo.xml for improving package metadata + +* Wed Nov 25 22:53:59 CET 2020 kb1000 +- Initial version of the RPM package, based on the Ubuntu package diff --git a/ultimmc/launcher/package/rpm/README.md b/ultimmc/launcher/package/rpm/README.md new file mode 100644 index 0000000..0c2b1e4 --- /dev/null +++ b/ultimmc/launcher/package/rpm/README.md @@ -0,0 +1,12 @@ +# What is this? +A simple RPM package for MultiMC that contains a script that downloads and installs real MultiMC on Red Hat based systems. + +It contains a `.desktop` file, a `.metainfo.xml` file, an icon, and a simple script that does the heavy lifting. + +# How to build this? +You need the `rpm-build` package. Switch into this directory, then run: +``` +rpmbuild --build-in-place -bb MultiMC5.spec +``` + +Replace the version with whatever is appropriate. diff --git a/ultimmc/launcher/package/ubuntu/README.md b/ultimmc/launcher/package/ubuntu/README.md new file mode 100644 index 0000000..ddc97ae --- /dev/null +++ b/ultimmc/launcher/package/ubuntu/README.md @@ -0,0 +1,14 @@ +# What is this? +A simple Ubuntu package for MultiMC that contains a script that downloads and installs real MultiMC on Ubuntu based systems. + +It contains a `.desktop` file, an icon, and a simple script that does the heavy lifting. + +This is also the source for the files in the [RPM package](../rpm). If you rename, create or delete files here, you'll likely also have to update the RPM spec file there. + +# How to build this? +You need dpkg utils. Rename the `multimc` folder to `multimc_1.6-1` and then run: +``` +fakeroot dpkg-deb --build multimc_1.6-1 +``` + +Replace the version with whatever is appropriate. diff --git a/ultimmc/launcher/package/ubuntu/multimc/DEBIAN/control b/ultimmc/launcher/package/ubuntu/multimc/DEBIAN/control new file mode 100644 index 0000000..bfa3f1f --- /dev/null +++ b/ultimmc/launcher/package/ubuntu/multimc/DEBIAN/control @@ -0,0 +1,12 @@ +Package: multimc +Version: 1.6-2 +Architecture: all +Maintainer: Petr Mrázek +Section: games +Priority: optional +Installed-Size: 75 +Depends: zenity, desktop-file-utils, libqt5widgets5, libqt5gui5, libqt5network5, libqt5core5a, libqt5xml5, libqt5concurrent5, wget +Recommends: openjdk-8-jre +Homepage: http://multimc.org +Description: A local install wrapper for MultiMC + diff --git a/ultimmc/launcher/package/ubuntu/multimc/DEBIAN/postrm b/ultimmc/launcher/package/ubuntu/multimc/DEBIAN/postrm new file mode 100755 index 0000000..f9bbc8a --- /dev/null +++ b/ultimmc/launcher/package/ubuntu/multimc/DEBIAN/postrm @@ -0,0 +1,3 @@ +#!/bin/sh +set -e +update-desktop-database diff --git a/ultimmc/launcher/package/ubuntu/multimc/opt/multimc/icon.svg b/ultimmc/launcher/package/ubuntu/multimc/opt/multimc/icon.svg new file mode 100644 index 0000000..8bb0e28 --- /dev/null +++ b/ultimmc/launcher/package/ubuntu/multimc/opt/multimc/icon.svg @@ -0,0 +1,353 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/package/ubuntu/multimc/opt/multimc/run.sh b/ultimmc/launcher/package/ubuntu/multimc/opt/multimc/run.sh new file mode 100755 index 0000000..12a9b45 --- /dev/null +++ b/ultimmc/launcher/package/ubuntu/multimc/opt/multimc/run.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +INSTDIR="${XDG_DATA_HOME-$HOME/.local/share}/multimc" + +if [ `getconf LONG_BIT` = "64" ] +then + PACKAGE="mmc-stable-lin64.tar.gz" +else + PACKAGE="mmc-stable-lin32.tar.gz" +fi + +deploy() { + mkdir -p $INSTDIR + cd ${INSTDIR} + + wget --progress=dot:force "https://files.multimc.org/downloads/${PACKAGE}" 2>&1 | sed -u 's/.* \([0-9]\+%\)\ \+\([0-9.]\+.\) \(.*\)/\1\n# Downloading at \2\/s, ETA \3/' | zenity --progress --auto-close --auto-kill --title="Downloading MultiMC..." + + tar -xzf ${PACKAGE} --transform='s,MultiMC/,,' + rm ${PACKAGE} + chmod +x MultiMC +} + +runmmc() { + cd ${INSTDIR} + exec ./MultiMC "$@" +} + +if [[ ! -f ${INSTDIR}/MultiMC ]]; then + deploy + runmmc "$@" +else + runmmc "$@" +fi diff --git a/ultimmc/launcher/package/ubuntu/multimc/usr/share/applications/multimc.desktop b/ultimmc/launcher/package/ubuntu/multimc/usr/share/applications/multimc.desktop new file mode 100644 index 0000000..e0456f8 --- /dev/null +++ b/ultimmc/launcher/package/ubuntu/multimc/usr/share/applications/multimc.desktop @@ -0,0 +1,16 @@ +[Desktop Entry] +Categories=Game; +Exec=/opt/multimc/run.sh +Icon=/opt/multimc/icon.svg +Keywords=game;Minecraft; +MimeType= +Name=MultiMC 5 +Path= +StartupNotify=true +Terminal=false +TerminalOptions= +Type=Application +X-DBUS-ServiceName= +X-DBUS-StartupType= +X-KDE-SubstituteUID=false +X-KDE-Username= diff --git a/ultimmc/launcher/package/ubuntu/multimc/usr/share/man/man1/multimc.1 b/ultimmc/launcher/package/ubuntu/multimc/usr/share/man/man1/multimc.1 new file mode 100644 index 0000000..b4af25e --- /dev/null +++ b/ultimmc/launcher/package/ubuntu/multimc/usr/share/man/man1/multimc.1 @@ -0,0 +1,97 @@ +'\" t +.\" Title: multimc +.\" Author: [see the "AUTHORS" section] +.\" Generator: DocBook XSL Stylesheets vsnapshot +.\" Date: 11/07/2021 +.\" Manual: \ \& +.\" Source: \ \& +.\" Language: English +.\" +.TH "MULTIMC" "1" "11/07/2021" "\ \&" "\ \&" +.\" ----------------------------------------------------------------- +.\" * Define some portability stuff +.\" ----------------------------------------------------------------- +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.\" http://bugs.debian.org/507673 +.\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html +.\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\" ----------------------------------------------------------------- +.\" * set default formatting +.\" ----------------------------------------------------------------- +.\" disable hyphenation +.nh +.\" disable justification (adjust text to left margin only) +.ad l +.\" ----------------------------------------------------------------- +.\" * MAIN CONTENT STARTS HERE * +.\" ----------------------------------------------------------------- +.SH "NAME" +multimc \- a launcher and instance manager for Minecraft\&. +.SH "SYNOPSIS" +.sp +\fBmultimc\fR [\fIOPTIONS\fR] +.SH "DESCRIPTION" +.sp +MultiMC is a custom launcher for Minecraft that allows you to easily manage multiple installations of Minecraft at once\&. It also allows you to easily install and remove mods by simply dragging and dropping\&. Here are the current features of MultiMC\&. +.SH "OPTIONS" +.PP +\fB\-d, \-\-dir\fR=\fIDIRECTORY\fR +.RS 4 +Use +\fIDIRECTORY\fR +as the MultiMC root\&. +.RE +.PP +\fB\-l, \-\-launch\fR=\fIINSTANCE_ID\fR +.RS 4 +Launch the instance specified by +\fIINSTANCE_ID\fR\&. +.RE +.PP +\fB\-\-alive\fR +.RS 4 +Write a small +\fIlive\&.check\fR +file after MultiMC starts\&. +.RE +.PP +\fB\-h, \-\-help\fR +.RS 4 +Display help text and exit\&. +.RE +.PP +\fB\-v, \-\-version\fR +.RS 4 +Display program version and exit\&. +.RE +.PP +\fB\-a, \-\-profile\fR=\fIPROFILE\fR +.RS 4 +Use the account specified by +\fIPROFILE\fR +(only valid in combination with \-\-launch)\&. +.RE +.SH "EXIT STATUS" +.PP +\fB0\fR +.RS 4 +Success +.RE +.PP +\fB1\fR +.RS 4 +Failure (syntax or usage error; configuration error; unexpected error)\&. +.RE +.SH "BUGS" +.sp +https://github\&.com/MultiMC/Launcher/issues +.SH "RESOURCES" +.sp +GitHub: https://github\&.com/MultiMC/Launcher +.sp +Main website: https://multimc\&.org +.SH "AUTHORS" +.sp +peterix diff --git a/ultimmc/launcher/package/ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml b/ultimmc/launcher/package/ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml new file mode 100644 index 0000000..3bccba4 --- /dev/null +++ b/ultimmc/launcher/package/ubuntu/multimc/usr/share/metainfo/multimc.metainfo.xml @@ -0,0 +1,54 @@ + + + multimc + multimc.desktop + MultiMC + Manage Minecraft instances with ease + +

Overview

+

MultiMC is a free, open source launcher for Minecraft. It allows you to have multiple, cleanly separated instances of Minecraft (each with their own mods, texture packs, saves, etc) and helps you manage them and their associated options with a simple and powerful interface.

+

Features

+
    +
  • Manage multiple instances of Minecraft at once
  • +
  • Start Minecraft with a custom resolution
  • +
  • Change Java's runtime options (including memory options)
  • +
  • Shows Minecraft's console output in a colour coded window
  • +
  • Kill Minecraft easily if it crashes / freezes
  • +
  • Custom icons and groups for instances
  • +
  • Forge integration (automatic installation, version downloads, mod management)
  • +
  • Minecraft world management
  • +
  • Import and export Minecraft instances to share them with anyone
  • +
  • Supports every version of Minecraft that the vanilla launcher does
  • +
+
+ + + https://multimc.org/images/screenshots/main.png + + + https://multimc.org/images/screenshots/editmods.png + + + https://multimc.org/images/screenshots/version.png + + + https://multimc.org/images/screenshots/console.png + + + https://multimc.org/images/screenshots/settings.png + + + + + + https://multimc.org/ + https://discord.com/invite/0k2zsXGNHs0fE4Wm + https://github.com/MultiMC/Launcher/wiki/FAQ + https://github.com/MultiMC/Launcher/issues + https://translate.multimc.org/ + https://www.patreon.com/multimc + The MultiMC Team + CC0-1.0 + Apache-2.0 + peterix_at_gmail.com +
diff --git a/ultimmc/launcher/pathmatcher/FSTreeMatcher.h b/ultimmc/launcher/pathmatcher/FSTreeMatcher.h new file mode 100644 index 0000000..b84af29 --- /dev/null +++ b/ultimmc/launcher/pathmatcher/FSTreeMatcher.h @@ -0,0 +1,21 @@ +#pragma once + +#include "IPathMatcher.h" +#include +#include + +class FSTreeMatcher : public IPathMatcher +{ +public: + virtual ~FSTreeMatcher() {}; + FSTreeMatcher(SeparatorPrefixTree<'/'> & tree) : m_fsTree(tree) + { + } + + bool matches(const QString &string) const override + { + return m_fsTree.covers(string); + } + + SeparatorPrefixTree<'/'> & m_fsTree; +}; diff --git a/ultimmc/launcher/pathmatcher/IPathMatcher.h b/ultimmc/launcher/pathmatcher/IPathMatcher.h new file mode 100644 index 0000000..192782d --- /dev/null +++ b/ultimmc/launcher/pathmatcher/IPathMatcher.h @@ -0,0 +1,13 @@ +#pragma once +#include +#include + +class IPathMatcher +{ +public: + typedef std::shared_ptr Ptr; + +public: + virtual ~IPathMatcher(){}; + virtual bool matches(const QString &string) const = 0; +}; diff --git a/ultimmc/launcher/pathmatcher/MultiMatcher.h b/ultimmc/launcher/pathmatcher/MultiMatcher.h new file mode 100644 index 0000000..8bc1b6e --- /dev/null +++ b/ultimmc/launcher/pathmatcher/MultiMatcher.h @@ -0,0 +1,31 @@ +#include "IPathMatcher.h" +#include +#include + +class MultiMatcher : public IPathMatcher +{ +public: + virtual ~MultiMatcher() {}; + MultiMatcher() + { + } + MultiMatcher &add(Ptr add) + { + m_matchers.append(add); + return *this; + } + + virtual bool matches(const QString &string) const override + { + for(auto iter: m_matchers) + { + if(iter->matches(string)) + { + return true; + } + } + return false; + } + + QList m_matchers; +}; diff --git a/ultimmc/launcher/pathmatcher/RegexpMatcher.h b/ultimmc/launcher/pathmatcher/RegexpMatcher.h new file mode 100644 index 0000000..825d488 --- /dev/null +++ b/ultimmc/launcher/pathmatcher/RegexpMatcher.h @@ -0,0 +1,42 @@ +#include "IPathMatcher.h" +#include + +class RegexpMatcher : public IPathMatcher +{ +public: + virtual ~RegexpMatcher() {}; + RegexpMatcher(const QString ®exp) + { + m_regexp.setPattern(regexp); + m_onlyFilenamePart = !regexp.contains('/'); + } + + RegexpMatcher &caseSensitive(bool cs = true) + { + if(cs) + { + m_regexp.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + else + { + m_regexp.setPatternOptions(QRegularExpression::NoPatternOption); + } + return *this; + } + + virtual bool matches(const QString &string) const override + { + if(m_onlyFilenamePart) + { + auto slash = string.lastIndexOf('/'); + if(slash != -1) + { + auto part = string.mid(slash + 1); + return m_regexp.match(part).hasMatch(); + } + } + return m_regexp.match(string).hasMatch(); + } + QRegularExpression m_regexp; + bool m_onlyFilenamePart = false; +}; diff --git a/ultimmc/launcher/resources/OSX/OSX.qrc b/ultimmc/launcher/resources/OSX/OSX.qrc new file mode 100644 index 0000000..b85313e --- /dev/null +++ b/ultimmc/launcher/resources/OSX/OSX.qrc @@ -0,0 +1,39 @@ + + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/launcher.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/patreon.svg + scalable/proxy.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/shaderpacks.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + + diff --git a/ultimmc/launcher/resources/OSX/index.theme b/ultimmc/launcher/resources/OSX/index.theme new file mode 100644 index 0000000..7f90a32 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=OSX +Comment=OSX theme by pexner +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/ultimmc/launcher/resources/OSX/scalable/about.svg b/ultimmc/launcher/resources/OSX/scalable/about.svg new file mode 100644 index 0000000..eb87ccf --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/about.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/accounts.svg b/ultimmc/launcher/resources/OSX/scalable/accounts.svg new file mode 100644 index 0000000..163bcee --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/accounts.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/bug.svg b/ultimmc/launcher/resources/OSX/scalable/bug.svg new file mode 100644 index 0000000..00565bb --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/bug.svg @@ -0,0 +1,25 @@ + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/centralmods.svg b/ultimmc/launcher/resources/OSX/scalable/centralmods.svg new file mode 100644 index 0000000..37b821e --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/centralmods.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/checkupdate.svg b/ultimmc/launcher/resources/OSX/scalable/checkupdate.svg new file mode 100644 index 0000000..30cec51 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/checkupdate.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/copy.svg b/ultimmc/launcher/resources/OSX/scalable/copy.svg new file mode 100644 index 0000000..7382d6e --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/copy.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/coremods.svg b/ultimmc/launcher/resources/OSX/scalable/coremods.svg new file mode 100644 index 0000000..b0df605 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/coremods.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/custom-commands.svg b/ultimmc/launcher/resources/OSX/scalable/custom-commands.svg new file mode 100644 index 0000000..e663452 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/custom-commands.svg @@ -0,0 +1,71 @@ + +image/svg+xml diff --git a/ultimmc/launcher/resources/OSX/scalable/externaltools.svg b/ultimmc/launcher/resources/OSX/scalable/externaltools.svg new file mode 100644 index 0000000..a2b7488 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/externaltools.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/help.svg b/ultimmc/launcher/resources/OSX/scalable/help.svg new file mode 100644 index 0000000..9d1b367 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/help.svg @@ -0,0 +1,51 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/OSX/scalable/instance-settings.svg b/ultimmc/launcher/resources/OSX/scalable/instance-settings.svg new file mode 100644 index 0000000..394877f --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/instance-settings.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/jarmods.svg b/ultimmc/launcher/resources/OSX/scalable/jarmods.svg new file mode 100644 index 0000000..213ec83 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/jarmods.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/java.svg b/ultimmc/launcher/resources/OSX/scalable/java.svg new file mode 100644 index 0000000..e1aee15 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/java.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/language.svg b/ultimmc/launcher/resources/OSX/scalable/language.svg new file mode 100644 index 0000000..4f7d002 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/language.svg @@ -0,0 +1,40 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/OSX/scalable/launcher.svg b/ultimmc/launcher/resources/OSX/scalable/launcher.svg new file mode 100644 index 0000000..e0aaad8 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/launcher.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/loadermods.svg b/ultimmc/launcher/resources/OSX/scalable/loadermods.svg new file mode 100644 index 0000000..76951eb --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/loadermods.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/log.svg b/ultimmc/launcher/resources/OSX/scalable/log.svg new file mode 100644 index 0000000..0ac45d5 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/log.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/minecraft.svg b/ultimmc/launcher/resources/OSX/scalable/minecraft.svg new file mode 100644 index 0000000..86c915b --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/minecraft.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/new.svg b/ultimmc/launcher/resources/OSX/scalable/new.svg new file mode 100644 index 0000000..79ee87b --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/new.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/news.svg b/ultimmc/launcher/resources/OSX/scalable/news.svg new file mode 100644 index 0000000..b8ce3cd --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/news.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/notes.svg b/ultimmc/launcher/resources/OSX/scalable/notes.svg new file mode 100644 index 0000000..c2e95cf --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/notes.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/patreon.svg b/ultimmc/launcher/resources/OSX/scalable/patreon.svg new file mode 100644 index 0000000..4f0da3e --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/patreon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/proxy.svg b/ultimmc/launcher/resources/OSX/scalable/proxy.svg new file mode 100644 index 0000000..99acaa2 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/proxy.svg @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/refresh.svg b/ultimmc/launcher/resources/OSX/scalable/refresh.svg new file mode 100644 index 0000000..c97489c --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/refresh.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/resourcepacks.svg b/ultimmc/launcher/resources/OSX/scalable/resourcepacks.svg new file mode 100644 index 0000000..c85d4e3 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/resourcepacks.svg @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/screenshots.svg b/ultimmc/launcher/resources/OSX/scalable/screenshots.svg new file mode 100644 index 0000000..12df0c8 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/screenshots.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/settings.svg b/ultimmc/launcher/resources/OSX/scalable/settings.svg new file mode 100644 index 0000000..dcdd9f1 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/settings.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/shaderpacks.svg b/ultimmc/launcher/resources/OSX/scalable/shaderpacks.svg new file mode 100644 index 0000000..cf8251b --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/shaderpacks.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/status-bad.svg b/ultimmc/launcher/resources/OSX/scalable/status-bad.svg new file mode 100644 index 0000000..add7a6f --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/status-bad.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/status-good.svg b/ultimmc/launcher/resources/OSX/scalable/status-good.svg new file mode 100644 index 0000000..f10da75 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/status-good.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/status-yellow.svg b/ultimmc/launcher/resources/OSX/scalable/status-yellow.svg new file mode 100644 index 0000000..fba697b --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/status-yellow.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/viewfolder.svg b/ultimmc/launcher/resources/OSX/scalable/viewfolder.svg new file mode 100644 index 0000000..682c72c --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/viewfolder.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/OSX/scalable/worlds.svg b/ultimmc/launcher/resources/OSX/scalable/worlds.svg new file mode 100644 index 0000000..b149127 --- /dev/null +++ b/ultimmc/launcher/resources/OSX/scalable/worlds.svg @@ -0,0 +1,58 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/assets/underconstruction.png b/ultimmc/launcher/resources/assets/underconstruction.png new file mode 100644 index 0000000..6ae0647 Binary files /dev/null and b/ultimmc/launcher/resources/assets/underconstruction.png differ diff --git a/ultimmc/launcher/resources/backgrounds/backgrounds.qrc b/ultimmc/launcher/resources/backgrounds/backgrounds.qrc new file mode 100644 index 0000000..5292151 --- /dev/null +++ b/ultimmc/launcher/resources/backgrounds/backgrounds.qrc @@ -0,0 +1,8 @@ + + + + catbgrnd2.png + catmas.png + cattiversary.png + + diff --git a/ultimmc/launcher/resources/backgrounds/catbgrnd2.png b/ultimmc/launcher/resources/backgrounds/catbgrnd2.png new file mode 100644 index 0000000..e9de7f2 Binary files /dev/null and b/ultimmc/launcher/resources/backgrounds/catbgrnd2.png differ diff --git a/ultimmc/launcher/resources/backgrounds/catmas.png b/ultimmc/launcher/resources/backgrounds/catmas.png new file mode 100644 index 0000000..8bdb1d5 Binary files /dev/null and b/ultimmc/launcher/resources/backgrounds/catmas.png differ diff --git a/ultimmc/launcher/resources/backgrounds/cattiversary.png b/ultimmc/launcher/resources/backgrounds/cattiversary.png new file mode 100644 index 0000000..09a3656 Binary files /dev/null and b/ultimmc/launcher/resources/backgrounds/cattiversary.png differ diff --git a/ultimmc/launcher/resources/documents/documents.qrc b/ultimmc/launcher/resources/documents/documents.qrc new file mode 100644 index 0000000..007efcd --- /dev/null +++ b/ultimmc/launcher/resources/documents/documents.qrc @@ -0,0 +1,7 @@ + + + + ../../../COPYING.md + + + diff --git a/ultimmc/launcher/resources/flat/flat.qrc b/ultimmc/launcher/resources/flat/flat.qrc new file mode 100644 index 0000000..a888f6d --- /dev/null +++ b/ultimmc/launcher/resources/flat/flat.qrc @@ -0,0 +1,47 @@ + + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/cat.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/discord.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/launcher.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/packages.svg + scalable/patreon.svg + scalable/proxy.svg + scalable/quickmods.svg + scalable/reddit-alien.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/shaderpacks.svg + scalable/screenshot-placeholder.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/star.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-running.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + + diff --git a/ultimmc/launcher/resources/flat/index.theme b/ultimmc/launcher/resources/flat/index.theme new file mode 100644 index 0000000..34e27aa --- /dev/null +++ b/ultimmc/launcher/resources/flat/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=Flat +Comment=Flat icons +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/ultimmc/launcher/resources/flat/scalable/about.svg b/ultimmc/launcher/resources/flat/scalable/about.svg new file mode 100644 index 0000000..4f85045 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/about.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/accounts.svg b/ultimmc/launcher/resources/flat/scalable/accounts.svg new file mode 100644 index 0000000..e6a1328 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/accounts.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/bug.svg b/ultimmc/launcher/resources/flat/scalable/bug.svg new file mode 100644 index 0000000..ea370fa --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/bug.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/cat.svg b/ultimmc/launcher/resources/flat/scalable/cat.svg new file mode 100644 index 0000000..e90763b --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/cat.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/centralmods.svg b/ultimmc/launcher/resources/flat/scalable/centralmods.svg new file mode 100644 index 0000000..c694662 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/centralmods.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/checkupdate.svg b/ultimmc/launcher/resources/flat/scalable/checkupdate.svg new file mode 100644 index 0000000..e6525a0 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/checkupdate.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/copy.svg b/ultimmc/launcher/resources/flat/scalable/copy.svg new file mode 100644 index 0000000..36986e0 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/copy.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/coremods.svg b/ultimmc/launcher/resources/flat/scalable/coremods.svg new file mode 100644 index 0000000..21a3450 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/coremods.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/custom-commands.svg b/ultimmc/launcher/resources/flat/scalable/custom-commands.svg new file mode 100644 index 0000000..a35634b --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/custom-commands.svg @@ -0,0 +1,86 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/flat/scalable/discord.svg b/ultimmc/launcher/resources/flat/scalable/discord.svg new file mode 100644 index 0000000..ad63180 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/discord.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/externaltools.svg b/ultimmc/launcher/resources/flat/scalable/externaltools.svg new file mode 100644 index 0000000..55820df --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/externaltools.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/help.svg b/ultimmc/launcher/resources/flat/scalable/help.svg new file mode 100644 index 0000000..26d5d7f --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/help.svg @@ -0,0 +1,17 @@ + + + + diff --git a/ultimmc/launcher/resources/flat/scalable/instance-settings.svg b/ultimmc/launcher/resources/flat/scalable/instance-settings.svg new file mode 100644 index 0000000..dd9d86e --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/instance-settings.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/jarmods.svg b/ultimmc/launcher/resources/flat/scalable/jarmods.svg new file mode 100644 index 0000000..db90fa3 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/jarmods.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/java.svg b/ultimmc/launcher/resources/flat/scalable/java.svg new file mode 100644 index 0000000..dc19ee2 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/java.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/language.svg b/ultimmc/launcher/resources/flat/scalable/language.svg new file mode 100644 index 0000000..f4d3f2f --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/language.svg @@ -0,0 +1,103 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/flat/scalable/launcher.svg b/ultimmc/launcher/resources/flat/scalable/launcher.svg new file mode 100644 index 0000000..2c4964d --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/launcher.svg @@ -0,0 +1,2 @@ + + diff --git a/ultimmc/launcher/resources/flat/scalable/loadermods.svg b/ultimmc/launcher/resources/flat/scalable/loadermods.svg new file mode 100644 index 0000000..8a2fd12 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/loadermods.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/log.svg b/ultimmc/launcher/resources/flat/scalable/log.svg new file mode 100644 index 0000000..e8caa08 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/log.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/minecraft.svg b/ultimmc/launcher/resources/flat/scalable/minecraft.svg new file mode 100644 index 0000000..c17c44c --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/minecraft.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/multimc.svg b/ultimmc/launcher/resources/flat/scalable/multimc.svg new file mode 100644 index 0000000..1c1f235 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/multimc.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/new.svg b/ultimmc/launcher/resources/flat/scalable/new.svg new file mode 100644 index 0000000..01f19d7 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/new.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/news.svg b/ultimmc/launcher/resources/flat/scalable/news.svg new file mode 100644 index 0000000..8868414 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/news.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/notes.svg b/ultimmc/launcher/resources/flat/scalable/notes.svg new file mode 100644 index 0000000..ebe0cb5 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/notes.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/packages.svg b/ultimmc/launcher/resources/flat/scalable/packages.svg new file mode 100644 index 0000000..fe576a4 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/packages.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/patreon.svg b/ultimmc/launcher/resources/flat/scalable/patreon.svg new file mode 100644 index 0000000..ad561f5 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/patreon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/proxy.svg b/ultimmc/launcher/resources/flat/scalable/proxy.svg new file mode 100644 index 0000000..4956fec --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/proxy.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/quickmods.svg b/ultimmc/launcher/resources/flat/scalable/quickmods.svg new file mode 100644 index 0000000..952d1e0 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/quickmods.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/reddit-alien.svg b/ultimmc/launcher/resources/flat/scalable/reddit-alien.svg new file mode 100644 index 0000000..9bcfbed --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/reddit-alien.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/refresh.svg b/ultimmc/launcher/resources/flat/scalable/refresh.svg new file mode 100644 index 0000000..94be1e2 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/refresh.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/resourcepacks.svg b/ultimmc/launcher/resources/flat/scalable/resourcepacks.svg new file mode 100644 index 0000000..b6054ba --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/resourcepacks.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/screenshot-placeholder.svg b/ultimmc/launcher/resources/flat/scalable/screenshot-placeholder.svg new file mode 100644 index 0000000..99e0c17 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/screenshot-placeholder.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/screenshots.svg b/ultimmc/launcher/resources/flat/scalable/screenshots.svg new file mode 100644 index 0000000..208bb10 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/screenshots.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/settings.svg b/ultimmc/launcher/resources/flat/scalable/settings.svg new file mode 100644 index 0000000..dd9d86e --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/settings.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/shaderpacks.svg b/ultimmc/launcher/resources/flat/scalable/shaderpacks.svg new file mode 100644 index 0000000..f1460bd --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/shaderpacks.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/flat/scalable/star.svg b/ultimmc/launcher/resources/flat/scalable/star.svg new file mode 100644 index 0000000..878bdca --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/star.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/status-bad.svg b/ultimmc/launcher/resources/flat/scalable/status-bad.svg new file mode 100644 index 0000000..3f8e011 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/status-bad.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/status-good.svg b/ultimmc/launcher/resources/flat/scalable/status-good.svg new file mode 100644 index 0000000..3503d6b --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/status-good.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/status-running.svg b/ultimmc/launcher/resources/flat/scalable/status-running.svg new file mode 100644 index 0000000..7c75031 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/status-running.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/status-yellow.svg b/ultimmc/launcher/resources/flat/scalable/status-yellow.svg new file mode 100644 index 0000000..ac2d234 --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/status-yellow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/viewfolder.svg b/ultimmc/launcher/resources/flat/scalable/viewfolder.svg new file mode 100644 index 0000000..2f5e29c --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/viewfolder.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/flat/scalable/worlds.svg b/ultimmc/launcher/resources/flat/scalable/worlds.svg new file mode 100644 index 0000000..95a59bd --- /dev/null +++ b/ultimmc/launcher/resources/flat/scalable/worlds.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/iOS/iOS.qrc b/ultimmc/launcher/resources/iOS/iOS.qrc new file mode 100644 index 0000000..5eec744 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/iOS.qrc @@ -0,0 +1,39 @@ + + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/launcher.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/patreon.svg + scalable/proxy.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/shaderpacks.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + + diff --git a/ultimmc/launcher/resources/iOS/index.theme b/ultimmc/launcher/resources/iOS/index.theme new file mode 100644 index 0000000..b0f2f6b --- /dev/null +++ b/ultimmc/launcher/resources/iOS/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=iOS +Comment=iOS theme by pexner +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/ultimmc/launcher/resources/iOS/scalable/about.svg b/ultimmc/launcher/resources/iOS/scalable/about.svg new file mode 100644 index 0000000..c4d3547 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/about.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/accounts.svg b/ultimmc/launcher/resources/iOS/scalable/accounts.svg new file mode 100644 index 0000000..65f76c3 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/accounts.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/bug.svg b/ultimmc/launcher/resources/iOS/scalable/bug.svg new file mode 100644 index 0000000..fc4a3d6 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/bug.svg @@ -0,0 +1,22 @@ + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/centralmods.svg b/ultimmc/launcher/resources/iOS/scalable/centralmods.svg new file mode 100644 index 0000000..1b4c474 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/centralmods.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/checkupdate.svg b/ultimmc/launcher/resources/iOS/scalable/checkupdate.svg new file mode 100644 index 0000000..9fc983d --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/checkupdate.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/copy.svg b/ultimmc/launcher/resources/iOS/scalable/copy.svg new file mode 100644 index 0000000..3ccc2f0 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/copy.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/coremods.svg b/ultimmc/launcher/resources/iOS/scalable/coremods.svg new file mode 100644 index 0000000..ea47872 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/coremods.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/custom-commands.svg b/ultimmc/launcher/resources/iOS/scalable/custom-commands.svg new file mode 100644 index 0000000..f44e2bf --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/custom-commands.svg @@ -0,0 +1,63 @@ + +image/svg+xml + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/externaltools.svg b/ultimmc/launcher/resources/iOS/scalable/externaltools.svg new file mode 100644 index 0000000..16e9fa4 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/externaltools.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/help.svg b/ultimmc/launcher/resources/iOS/scalable/help.svg new file mode 100644 index 0000000..9c2d2e9 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/help.svg @@ -0,0 +1,38 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/iOS/scalable/instance-settings.svg b/ultimmc/launcher/resources/iOS/scalable/instance-settings.svg new file mode 100644 index 0000000..95b8a50 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/instance-settings.svg @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/jarmods.svg b/ultimmc/launcher/resources/iOS/scalable/jarmods.svg new file mode 100644 index 0000000..c4c5ca8 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/jarmods.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/java.svg b/ultimmc/launcher/resources/iOS/scalable/java.svg new file mode 100644 index 0000000..8d7c279 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/java.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/language.svg b/ultimmc/launcher/resources/iOS/scalable/language.svg new file mode 100644 index 0000000..fcc3436 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/language.svg @@ -0,0 +1,32 @@ + +image/svg+xml + + + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/iOS/scalable/launcher.svg b/ultimmc/launcher/resources/iOS/scalable/launcher.svg new file mode 100644 index 0000000..cd63ba7 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/launcher.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/loadermods.svg b/ultimmc/launcher/resources/iOS/scalable/loadermods.svg new file mode 100644 index 0000000..010efa1 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/loadermods.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/log.svg b/ultimmc/launcher/resources/iOS/scalable/log.svg new file mode 100644 index 0000000..5d1c7f0 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/log.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/minecraft.svg b/ultimmc/launcher/resources/iOS/scalable/minecraft.svg new file mode 100644 index 0000000..069b4e7 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/minecraft.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/multimc.svg b/ultimmc/launcher/resources/iOS/scalable/multimc.svg new file mode 100644 index 0000000..bc81943 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/multimc.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/new.svg b/ultimmc/launcher/resources/iOS/scalable/new.svg new file mode 100644 index 0000000..9f22158 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/new.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/news.svg b/ultimmc/launcher/resources/iOS/scalable/news.svg new file mode 100644 index 0000000..d3c010b --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/news.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/notes.svg b/ultimmc/launcher/resources/iOS/scalable/notes.svg new file mode 100644 index 0000000..b42ebee --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/notes.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/patreon.svg b/ultimmc/launcher/resources/iOS/scalable/patreon.svg new file mode 100644 index 0000000..1bd06f4 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/patreon.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/proxy.svg b/ultimmc/launcher/resources/iOS/scalable/proxy.svg new file mode 100644 index 0000000..f655228 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/proxy.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/refresh.svg b/ultimmc/launcher/resources/iOS/scalable/refresh.svg new file mode 100644 index 0000000..297b79c --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/refresh.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/resourcepacks.svg b/ultimmc/launcher/resources/iOS/scalable/resourcepacks.svg new file mode 100644 index 0000000..5b359d6 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/resourcepacks.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/screenshots.svg b/ultimmc/launcher/resources/iOS/scalable/screenshots.svg new file mode 100644 index 0000000..39ce7b8 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/screenshots.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/settings.svg b/ultimmc/launcher/resources/iOS/scalable/settings.svg new file mode 100644 index 0000000..95b8a50 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/settings.svg @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/shaderpacks.svg b/ultimmc/launcher/resources/iOS/scalable/shaderpacks.svg new file mode 100644 index 0000000..a2aa1b2 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/shaderpacks.svg @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/status-bad.svg b/ultimmc/launcher/resources/iOS/scalable/status-bad.svg new file mode 100644 index 0000000..4019c8d --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/status-bad.svg @@ -0,0 +1,10 @@ + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/status-good.svg b/ultimmc/launcher/resources/iOS/scalable/status-good.svg new file mode 100644 index 0000000..e185911 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/status-good.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/status-yellow.svg b/ultimmc/launcher/resources/iOS/scalable/status-yellow.svg new file mode 100644 index 0000000..d8a28e2 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/status-yellow.svg @@ -0,0 +1,56 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/iOS/scalable/viewfolder.svg b/ultimmc/launcher/resources/iOS/scalable/viewfolder.svg new file mode 100644 index 0000000..0ae0c0b --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/viewfolder.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/iOS/scalable/worlds.svg b/ultimmc/launcher/resources/iOS/scalable/worlds.svg new file mode 100644 index 0000000..1596fd7 --- /dev/null +++ b/ultimmc/launcher/resources/iOS/scalable/worlds.svg @@ -0,0 +1,44 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/chicken.png b/ultimmc/launcher/resources/multimc/128x128/instances/chicken.png new file mode 100644 index 0000000..71f6ded Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/chicken.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/creeper.png b/ultimmc/launcher/resources/multimc/128x128/instances/creeper.png new file mode 100644 index 0000000..41b7d07 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/creeper.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/enderpearl.png b/ultimmc/launcher/resources/multimc/128x128/instances/enderpearl.png new file mode 100644 index 0000000..0a5bf91 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/enderpearl.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/flame.png b/ultimmc/launcher/resources/multimc/128x128/instances/flame.png new file mode 100644 index 0000000..8a50a0b Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/flame.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/ftb_glow.png b/ultimmc/launcher/resources/multimc/128x128/instances/ftb_glow.png new file mode 100644 index 0000000..86632b2 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/ftb_glow.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/ftb_logo.png b/ultimmc/launcher/resources/multimc/128x128/instances/ftb_logo.png new file mode 100644 index 0000000..e725b7f Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/ftb_logo.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/gear.png b/ultimmc/launcher/resources/multimc/128x128/instances/gear.png new file mode 100644 index 0000000..75c68a6 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/gear.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/herobrine.png b/ultimmc/launcher/resources/multimc/128x128/instances/herobrine.png new file mode 100644 index 0000000..13f1494 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/herobrine.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/infinity.png b/ultimmc/launcher/resources/multimc/128x128/instances/infinity.png new file mode 100644 index 0000000..63e06e5 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/infinity.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/magitech.png b/ultimmc/launcher/resources/multimc/128x128/instances/magitech.png new file mode 100644 index 0000000..0f81a19 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/magitech.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/meat.png b/ultimmc/launcher/resources/multimc/128x128/instances/meat.png new file mode 100644 index 0000000..fefc9bf Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/meat.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/netherstar.png b/ultimmc/launcher/resources/multimc/128x128/instances/netherstar.png new file mode 100644 index 0000000..132085f Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/netherstar.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/skeleton.png b/ultimmc/launcher/resources/multimc/128x128/instances/skeleton.png new file mode 100644 index 0000000..55fcf5a Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/skeleton.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/squarecreeper.png b/ultimmc/launcher/resources/multimc/128x128/instances/squarecreeper.png new file mode 100644 index 0000000..c82d840 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/squarecreeper.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/instances/steve.png b/ultimmc/launcher/resources/multimc/128x128/instances/steve.png new file mode 100644 index 0000000..a07cbd2 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/instances/steve.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/shaderpacks.png b/ultimmc/launcher/resources/multimc/128x128/shaderpacks.png new file mode 100644 index 0000000..1de0e91 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/shaderpacks.png differ diff --git a/ultimmc/launcher/resources/multimc/128x128/unknown_server.png b/ultimmc/launcher/resources/multimc/128x128/unknown_server.png new file mode 100644 index 0000000..ec98382 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/128x128/unknown_server.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/about.png b/ultimmc/launcher/resources/multimc/16x16/about.png new file mode 100644 index 0000000..a6a986e Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/about.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/bug.png b/ultimmc/launcher/resources/multimc/16x16/bug.png new file mode 100644 index 0000000..0c5b78b Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/bug.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/cat.png b/ultimmc/launcher/resources/multimc/16x16/cat.png new file mode 100644 index 0000000..e6e31b4 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/cat.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/centralmods.png b/ultimmc/launcher/resources/multimc/16x16/centralmods.png new file mode 100644 index 0000000..c1b91c7 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/centralmods.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/checkupdate.png b/ultimmc/launcher/resources/multimc/16x16/checkupdate.png new file mode 100644 index 0000000..f374205 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/checkupdate.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/copy.png b/ultimmc/launcher/resources/multimc/16x16/copy.png new file mode 100644 index 0000000..ccaed9e Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/copy.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/coremods.png b/ultimmc/launcher/resources/multimc/16x16/coremods.png new file mode 100644 index 0000000..af0f116 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/coremods.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/help.png b/ultimmc/launcher/resources/multimc/16x16/help.png new file mode 100644 index 0000000..e6edf6b Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/help.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/instance-settings.png b/ultimmc/launcher/resources/multimc/16x16/instance-settings.png new file mode 100644 index 0000000..b916cd2 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/instance-settings.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/jarmods.png b/ultimmc/launcher/resources/multimc/16x16/jarmods.png new file mode 100644 index 0000000..1a97c9c Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/jarmods.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/loadermods.png b/ultimmc/launcher/resources/multimc/16x16/loadermods.png new file mode 100644 index 0000000..b5ab3fc Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/loadermods.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/log.png b/ultimmc/launcher/resources/multimc/16x16/log.png new file mode 100644 index 0000000..efa2a0b Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/log.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/minecraft.png b/ultimmc/launcher/resources/multimc/16x16/minecraft.png new file mode 100644 index 0000000..e9f2f2a Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/minecraft.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/new.png b/ultimmc/launcher/resources/multimc/16x16/new.png new file mode 100644 index 0000000..2e56f58 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/new.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/news.png b/ultimmc/launcher/resources/multimc/16x16/news.png new file mode 100644 index 0000000..872e85d Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/news.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/noaccount.png b/ultimmc/launcher/resources/multimc/16x16/noaccount.png new file mode 100644 index 0000000..b49bcf3 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/noaccount.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/patreon.png b/ultimmc/launcher/resources/multimc/16x16/patreon.png new file mode 100644 index 0000000..9150c47 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/patreon.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/refresh.png b/ultimmc/launcher/resources/multimc/16x16/refresh.png new file mode 100644 index 0000000..86b6f82 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/refresh.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/resourcepacks.png b/ultimmc/launcher/resources/multimc/16x16/resourcepacks.png new file mode 100644 index 0000000..d862f5c Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/resourcepacks.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/screenshots.png b/ultimmc/launcher/resources/multimc/16x16/screenshots.png new file mode 100644 index 0000000..460000d Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/screenshots.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/settings.png b/ultimmc/launcher/resources/multimc/16x16/settings.png new file mode 100644 index 0000000..b916cd2 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/settings.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/star.png b/ultimmc/launcher/resources/multimc/16x16/star.png new file mode 100644 index 0000000..4963e6e Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/star.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/status-bad.png b/ultimmc/launcher/resources/multimc/16x16/status-bad.png new file mode 100644 index 0000000..5b3f205 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/status-bad.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/status-good.png b/ultimmc/launcher/resources/multimc/16x16/status-good.png new file mode 100644 index 0000000..5cbdee8 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/status-good.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/status-running.png b/ultimmc/launcher/resources/multimc/16x16/status-running.png new file mode 100644 index 0000000..a4c42e3 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/status-running.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/status-yellow.png b/ultimmc/launcher/resources/multimc/16x16/status-yellow.png new file mode 100644 index 0000000..b25375d Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/status-yellow.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/viewfolder.png b/ultimmc/launcher/resources/multimc/16x16/viewfolder.png new file mode 100644 index 0000000..98b8a94 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/viewfolder.png differ diff --git a/ultimmc/launcher/resources/multimc/16x16/worlds.png b/ultimmc/launcher/resources/multimc/16x16/worlds.png new file mode 100644 index 0000000..1a38f38 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/16x16/worlds.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/about.png b/ultimmc/launcher/resources/multimc/22x22/about.png new file mode 100644 index 0000000..57775e2 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/about.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/bug.png b/ultimmc/launcher/resources/multimc/22x22/bug.png new file mode 100644 index 0000000..90481bb Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/bug.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/cat.png b/ultimmc/launcher/resources/multimc/22x22/cat.png new file mode 100644 index 0000000..3ea7ba6 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/cat.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/centralmods.png b/ultimmc/launcher/resources/multimc/22x22/centralmods.png new file mode 100644 index 0000000..a10f9a2 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/centralmods.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/checkupdate.png b/ultimmc/launcher/resources/multimc/22x22/checkupdate.png new file mode 100644 index 0000000..badb200 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/checkupdate.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/copy.png b/ultimmc/launcher/resources/multimc/22x22/copy.png new file mode 100644 index 0000000..ea236a2 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/copy.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/help.png b/ultimmc/launcher/resources/multimc/22x22/help.png new file mode 100644 index 0000000..da79b3e Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/help.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/instance-settings.png b/ultimmc/launcher/resources/multimc/22x22/instance-settings.png new file mode 100644 index 0000000..daf56aa Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/instance-settings.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/new.png b/ultimmc/launcher/resources/multimc/22x22/new.png new file mode 100644 index 0000000..c707fbb Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/new.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/news.png b/ultimmc/launcher/resources/multimc/22x22/news.png new file mode 100644 index 0000000..1953bf7 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/news.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/patreon.png b/ultimmc/launcher/resources/multimc/22x22/patreon.png new file mode 100644 index 0000000..f2c2076 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/patreon.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/refresh.png b/ultimmc/launcher/resources/multimc/22x22/refresh.png new file mode 100644 index 0000000..45b5535 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/refresh.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/screenshots.png b/ultimmc/launcher/resources/multimc/22x22/screenshots.png new file mode 100644 index 0000000..6fb42bb Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/screenshots.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/settings.png b/ultimmc/launcher/resources/multimc/22x22/settings.png new file mode 100644 index 0000000..daf56aa Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/settings.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/status-bad.png b/ultimmc/launcher/resources/multimc/22x22/status-bad.png new file mode 100644 index 0000000..2707539 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/status-bad.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/status-good.png b/ultimmc/launcher/resources/multimc/22x22/status-good.png new file mode 100644 index 0000000..f55debc Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/status-good.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/status-running.png b/ultimmc/launcher/resources/multimc/22x22/status-running.png new file mode 100644 index 0000000..0dffba1 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/status-running.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/status-yellow.png b/ultimmc/launcher/resources/multimc/22x22/status-yellow.png new file mode 100644 index 0000000..481eb7f Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/status-yellow.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/viewfolder.png b/ultimmc/launcher/resources/multimc/22x22/viewfolder.png new file mode 100644 index 0000000..b645167 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/viewfolder.png differ diff --git a/ultimmc/launcher/resources/multimc/22x22/worlds.png b/ultimmc/launcher/resources/multimc/22x22/worlds.png new file mode 100644 index 0000000..e8825ba Binary files /dev/null and b/ultimmc/launcher/resources/multimc/22x22/worlds.png differ diff --git a/ultimmc/launcher/resources/multimc/24x24/cat.png b/ultimmc/launcher/resources/multimc/24x24/cat.png new file mode 100644 index 0000000..c93245f Binary files /dev/null and b/ultimmc/launcher/resources/multimc/24x24/cat.png differ diff --git a/ultimmc/launcher/resources/multimc/24x24/coremods.png b/ultimmc/launcher/resources/multimc/24x24/coremods.png new file mode 100644 index 0000000..90603d2 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/24x24/coremods.png differ diff --git a/ultimmc/launcher/resources/multimc/24x24/jarmods.png b/ultimmc/launcher/resources/multimc/24x24/jarmods.png new file mode 100644 index 0000000..68cb8e9 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/24x24/jarmods.png differ diff --git a/ultimmc/launcher/resources/multimc/24x24/loadermods.png b/ultimmc/launcher/resources/multimc/24x24/loadermods.png new file mode 100644 index 0000000..250a626 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/24x24/loadermods.png differ diff --git a/ultimmc/launcher/resources/multimc/24x24/log.png b/ultimmc/launcher/resources/multimc/24x24/log.png new file mode 100644 index 0000000..fe30205 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/24x24/log.png differ diff --git a/ultimmc/launcher/resources/multimc/24x24/minecraft.png b/ultimmc/launcher/resources/multimc/24x24/minecraft.png new file mode 100644 index 0000000..b31177c Binary files /dev/null and b/ultimmc/launcher/resources/multimc/24x24/minecraft.png differ diff --git a/ultimmc/launcher/resources/multimc/24x24/noaccount.png b/ultimmc/launcher/resources/multimc/24x24/noaccount.png new file mode 100644 index 0000000..ac12437 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/24x24/noaccount.png differ diff --git a/ultimmc/launcher/resources/multimc/24x24/patreon.png b/ultimmc/launcher/resources/multimc/24x24/patreon.png new file mode 100644 index 0000000..add8066 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/24x24/patreon.png differ diff --git a/ultimmc/launcher/resources/multimc/24x24/resourcepacks.png b/ultimmc/launcher/resources/multimc/24x24/resourcepacks.png new file mode 100644 index 0000000..68359d3 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/24x24/resourcepacks.png differ diff --git a/ultimmc/launcher/resources/multimc/24x24/star.png b/ultimmc/launcher/resources/multimc/24x24/star.png new file mode 100644 index 0000000..7f16618 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/24x24/star.png differ diff --git a/ultimmc/launcher/resources/multimc/24x24/status-bad.png b/ultimmc/launcher/resources/multimc/24x24/status-bad.png new file mode 100644 index 0000000..d1547a4 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/24x24/status-bad.png differ diff --git a/ultimmc/launcher/resources/multimc/24x24/status-good.png b/ultimmc/launcher/resources/multimc/24x24/status-good.png new file mode 100644 index 0000000..3545bc4 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/24x24/status-good.png differ diff --git a/ultimmc/launcher/resources/multimc/24x24/status-running.png b/ultimmc/launcher/resources/multimc/24x24/status-running.png new file mode 100644 index 0000000..ecd6445 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/24x24/status-running.png differ diff --git a/ultimmc/launcher/resources/multimc/24x24/status-yellow.png b/ultimmc/launcher/resources/multimc/24x24/status-yellow.png new file mode 100644 index 0000000..dd5fde6 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/24x24/status-yellow.png differ diff --git a/ultimmc/launcher/resources/multimc/256x256/minecraft.png b/ultimmc/launcher/resources/multimc/256x256/minecraft.png new file mode 100644 index 0000000..77e3f03 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/256x256/minecraft.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/about.png b/ultimmc/launcher/resources/multimc/32x32/about.png new file mode 100644 index 0000000..5174c4f Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/about.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/bug.png b/ultimmc/launcher/resources/multimc/32x32/bug.png new file mode 100644 index 0000000..ada4665 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/bug.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/cat.png b/ultimmc/launcher/resources/multimc/32x32/cat.png new file mode 100644 index 0000000..78ff98e Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/cat.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/centralmods.png b/ultimmc/launcher/resources/multimc/32x32/centralmods.png new file mode 100644 index 0000000..cd2b820 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/centralmods.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/checkupdate.png b/ultimmc/launcher/resources/multimc/32x32/checkupdate.png new file mode 100644 index 0000000..754005f Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/checkupdate.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/copy.png b/ultimmc/launcher/resources/multimc/32x32/copy.png new file mode 100644 index 0000000..c137b0f Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/copy.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/coremods.png b/ultimmc/launcher/resources/multimc/32x32/coremods.png new file mode 100644 index 0000000..770d695 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/coremods.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/help.png b/ultimmc/launcher/resources/multimc/32x32/help.png new file mode 100644 index 0000000..b385427 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/help.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instance-settings.png b/ultimmc/launcher/resources/multimc/32x32/instance-settings.png new file mode 100644 index 0000000..a9c0817 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instance-settings.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/brick.png b/ultimmc/launcher/resources/multimc/32x32/instances/brick.png new file mode 100644 index 0000000..c324fda Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/brick.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/chicken.png b/ultimmc/launcher/resources/multimc/32x32/instances/chicken.png new file mode 100644 index 0000000..f870467 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/chicken.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/creeper.png b/ultimmc/launcher/resources/multimc/32x32/instances/creeper.png new file mode 100644 index 0000000..a67ecfc Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/creeper.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/diamond.png b/ultimmc/launcher/resources/multimc/32x32/instances/diamond.png new file mode 100644 index 0000000..1eb2646 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/diamond.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/dirt.png b/ultimmc/launcher/resources/multimc/32x32/instances/dirt.png new file mode 100644 index 0000000..9e19eb8 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/dirt.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/enderpearl.png b/ultimmc/launcher/resources/multimc/32x32/instances/enderpearl.png new file mode 100644 index 0000000..a818eb8 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/enderpearl.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/flame.png b/ultimmc/launcher/resources/multimc/32x32/instances/flame.png new file mode 100644 index 0000000..d898733 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/flame.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/ftb_glow.png b/ultimmc/launcher/resources/multimc/32x32/instances/ftb_glow.png new file mode 100644 index 0000000..c4e6fd5 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/ftb_glow.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/ftb_logo.png b/ultimmc/launcher/resources/multimc/32x32/instances/ftb_logo.png new file mode 100644 index 0000000..20df717 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/ftb_logo.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/gear.png b/ultimmc/launcher/resources/multimc/32x32/instances/gear.png new file mode 100644 index 0000000..da9ba2f Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/gear.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/gold.png b/ultimmc/launcher/resources/multimc/32x32/instances/gold.png new file mode 100644 index 0000000..593410f Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/gold.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/grass.png b/ultimmc/launcher/resources/multimc/32x32/instances/grass.png new file mode 100644 index 0000000..f169454 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/grass.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/herobrine.png b/ultimmc/launcher/resources/multimc/32x32/instances/herobrine.png new file mode 100644 index 0000000..e5460da Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/herobrine.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/infinity.png b/ultimmc/launcher/resources/multimc/32x32/instances/infinity.png new file mode 100644 index 0000000..bd94a3d Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/infinity.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/iron.png b/ultimmc/launcher/resources/multimc/32x32/instances/iron.png new file mode 100644 index 0000000..3e811bd Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/iron.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/magitech.png b/ultimmc/launcher/resources/multimc/32x32/instances/magitech.png new file mode 100644 index 0000000..6fd8ff6 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/magitech.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/meat.png b/ultimmc/launcher/resources/multimc/32x32/instances/meat.png new file mode 100644 index 0000000..6694859 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/meat.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/netherstar.png b/ultimmc/launcher/resources/multimc/32x32/instances/netherstar.png new file mode 100644 index 0000000..43cb511 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/netherstar.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/planks.png b/ultimmc/launcher/resources/multimc/32x32/instances/planks.png new file mode 100644 index 0000000..a94b750 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/planks.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/skeleton.png b/ultimmc/launcher/resources/multimc/32x32/instances/skeleton.png new file mode 100644 index 0000000..0c8d350 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/skeleton.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/squarecreeper.png b/ultimmc/launcher/resources/multimc/32x32/instances/squarecreeper.png new file mode 100644 index 0000000..b78c4ae Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/squarecreeper.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/steve.png b/ultimmc/launcher/resources/multimc/32x32/instances/steve.png new file mode 100644 index 0000000..07c6acd Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/steve.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/stone.png b/ultimmc/launcher/resources/multimc/32x32/instances/stone.png new file mode 100644 index 0000000..1b6ef7a Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/stone.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/instances/tnt.png b/ultimmc/launcher/resources/multimc/32x32/instances/tnt.png new file mode 100644 index 0000000..e40d404 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/instances/tnt.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/jarmods.png b/ultimmc/launcher/resources/multimc/32x32/jarmods.png new file mode 100644 index 0000000..5cda173 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/jarmods.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/loadermods.png b/ultimmc/launcher/resources/multimc/32x32/loadermods.png new file mode 100644 index 0000000..c4ca12e Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/loadermods.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/log.png b/ultimmc/launcher/resources/multimc/32x32/log.png new file mode 100644 index 0000000..d620da1 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/log.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/minecraft.png b/ultimmc/launcher/resources/multimc/32x32/minecraft.png new file mode 100644 index 0000000..816bec9 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/minecraft.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/new.png b/ultimmc/launcher/resources/multimc/32x32/new.png new file mode 100644 index 0000000..a3555ba Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/new.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/news.png b/ultimmc/launcher/resources/multimc/32x32/news.png new file mode 100644 index 0000000..c579fd4 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/news.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/noaccount.png b/ultimmc/launcher/resources/multimc/32x32/noaccount.png new file mode 100644 index 0000000..a73afc9 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/noaccount.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/patreon.png b/ultimmc/launcher/resources/multimc/32x32/patreon.png new file mode 100644 index 0000000..70085aa Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/patreon.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/refresh.png b/ultimmc/launcher/resources/multimc/32x32/refresh.png new file mode 100644 index 0000000..afa2a9d Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/refresh.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/resourcepacks.png b/ultimmc/launcher/resources/multimc/32x32/resourcepacks.png new file mode 100644 index 0000000..c14759e Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/resourcepacks.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/screenshots.png b/ultimmc/launcher/resources/multimc/32x32/screenshots.png new file mode 100644 index 0000000..4fcd622 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/screenshots.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/settings.png b/ultimmc/launcher/resources/multimc/32x32/settings.png new file mode 100644 index 0000000..a9c0817 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/settings.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/star.png b/ultimmc/launcher/resources/multimc/32x32/star.png new file mode 100644 index 0000000..b271f0d Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/star.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/status-bad.png b/ultimmc/launcher/resources/multimc/32x32/status-bad.png new file mode 100644 index 0000000..8c2c9d4 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/status-bad.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/status-good.png b/ultimmc/launcher/resources/multimc/32x32/status-good.png new file mode 100644 index 0000000..1a805e6 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/status-good.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/status-running.png b/ultimmc/launcher/resources/multimc/32x32/status-running.png new file mode 100644 index 0000000..f561f01 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/status-running.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/status-yellow.png b/ultimmc/launcher/resources/multimc/32x32/status-yellow.png new file mode 100644 index 0000000..42c6855 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/status-yellow.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/viewfolder.png b/ultimmc/launcher/resources/multimc/32x32/viewfolder.png new file mode 100644 index 0000000..74ab8fa Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/viewfolder.png differ diff --git a/ultimmc/launcher/resources/multimc/32x32/worlds.png b/ultimmc/launcher/resources/multimc/32x32/worlds.png new file mode 100644 index 0000000..c986596 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/32x32/worlds.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/about.png b/ultimmc/launcher/resources/multimc/48x48/about.png new file mode 100644 index 0000000..b4ac71b Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/about.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/bug.png b/ultimmc/launcher/resources/multimc/48x48/bug.png new file mode 100644 index 0000000..298f939 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/bug.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/cat.png b/ultimmc/launcher/resources/multimc/48x48/cat.png new file mode 100644 index 0000000..25912a3 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/cat.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/centralmods.png b/ultimmc/launcher/resources/multimc/48x48/centralmods.png new file mode 100644 index 0000000..d927e39 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/centralmods.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/checkupdate.png b/ultimmc/launcher/resources/multimc/48x48/checkupdate.png new file mode 100644 index 0000000..2e2c7d6 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/checkupdate.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/copy.png b/ultimmc/launcher/resources/multimc/48x48/copy.png new file mode 100644 index 0000000..ea40e34 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/copy.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/help.png b/ultimmc/launcher/resources/multimc/48x48/help.png new file mode 100644 index 0000000..82d828f Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/help.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/instance-settings.png b/ultimmc/launcher/resources/multimc/48x48/instance-settings.png new file mode 100644 index 0000000..6674eb2 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/instance-settings.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/log.png b/ultimmc/launcher/resources/multimc/48x48/log.png new file mode 100644 index 0000000..45f60e6 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/log.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/minecraft.png b/ultimmc/launcher/resources/multimc/48x48/minecraft.png new file mode 100644 index 0000000..38fc9f6 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/minecraft.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/new.png b/ultimmc/launcher/resources/multimc/48x48/new.png new file mode 100644 index 0000000..a81753b Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/new.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/news.png b/ultimmc/launcher/resources/multimc/48x48/news.png new file mode 100644 index 0000000..0f82d85 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/news.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/noaccount.png b/ultimmc/launcher/resources/multimc/48x48/noaccount.png new file mode 100644 index 0000000..4703796 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/noaccount.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/patreon.png b/ultimmc/launcher/resources/multimc/48x48/patreon.png new file mode 100644 index 0000000..7aec4d7 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/patreon.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/refresh.png b/ultimmc/launcher/resources/multimc/48x48/refresh.png new file mode 100644 index 0000000..0b08b23 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/refresh.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/screenshots.png b/ultimmc/launcher/resources/multimc/48x48/screenshots.png new file mode 100644 index 0000000..03c0059 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/screenshots.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/settings.png b/ultimmc/launcher/resources/multimc/48x48/settings.png new file mode 100644 index 0000000..6674eb2 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/settings.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/star.png b/ultimmc/launcher/resources/multimc/48x48/star.png new file mode 100644 index 0000000..d9468e7 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/star.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/status-bad.png b/ultimmc/launcher/resources/multimc/48x48/status-bad.png new file mode 100644 index 0000000..41c9cf2 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/status-bad.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/status-good.png b/ultimmc/launcher/resources/multimc/48x48/status-good.png new file mode 100644 index 0000000..df7cb59 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/status-good.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/status-running.png b/ultimmc/launcher/resources/multimc/48x48/status-running.png new file mode 100644 index 0000000..b8c0bf7 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/status-running.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/status-yellow.png b/ultimmc/launcher/resources/multimc/48x48/status-yellow.png new file mode 100644 index 0000000..4f3b11d Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/status-yellow.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/viewfolder.png b/ultimmc/launcher/resources/multimc/48x48/viewfolder.png new file mode 100644 index 0000000..0492a73 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/viewfolder.png differ diff --git a/ultimmc/launcher/resources/multimc/48x48/worlds.png b/ultimmc/launcher/resources/multimc/48x48/worlds.png new file mode 100644 index 0000000..4fc3375 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/48x48/worlds.png differ diff --git a/ultimmc/launcher/resources/multimc/50x50/instances/enderman.png b/ultimmc/launcher/resources/multimc/50x50/instances/enderman.png new file mode 100644 index 0000000..9f3a72b Binary files /dev/null and b/ultimmc/launcher/resources/multimc/50x50/instances/enderman.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/about.png b/ultimmc/launcher/resources/multimc/64x64/about.png new file mode 100644 index 0000000..b83e926 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/about.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/bug.png b/ultimmc/launcher/resources/multimc/64x64/bug.png new file mode 100644 index 0000000..156b031 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/bug.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/cat.png b/ultimmc/launcher/resources/multimc/64x64/cat.png new file mode 100644 index 0000000..2cc21f8 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/cat.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/centralmods.png b/ultimmc/launcher/resources/multimc/64x64/centralmods.png new file mode 100644 index 0000000..8831f43 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/centralmods.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/checkupdate.png b/ultimmc/launcher/resources/multimc/64x64/checkupdate.png new file mode 100644 index 0000000..dd1e29a Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/checkupdate.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/copy.png b/ultimmc/launcher/resources/multimc/64x64/copy.png new file mode 100644 index 0000000..d12cf9c Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/copy.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/coremods.png b/ultimmc/launcher/resources/multimc/64x64/coremods.png new file mode 100644 index 0000000..668be33 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/coremods.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/help.png b/ultimmc/launcher/resources/multimc/64x64/help.png new file mode 100644 index 0000000..0f3948c Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/help.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/instance-settings.png b/ultimmc/launcher/resources/multimc/64x64/instance-settings.png new file mode 100644 index 0000000..e3ff58f Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/instance-settings.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/jarmods.png b/ultimmc/launcher/resources/multimc/64x64/jarmods.png new file mode 100644 index 0000000..55d1a42 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/jarmods.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/loadermods.png b/ultimmc/launcher/resources/multimc/64x64/loadermods.png new file mode 100644 index 0000000..24618fd Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/loadermods.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/log.png b/ultimmc/launcher/resources/multimc/64x64/log.png new file mode 100644 index 0000000..0f531cd Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/log.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/new.png b/ultimmc/launcher/resources/multimc/64x64/new.png new file mode 100644 index 0000000..c3c6796 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/new.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/news.png b/ultimmc/launcher/resources/multimc/64x64/news.png new file mode 100644 index 0000000..e306eed Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/news.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/patreon.png b/ultimmc/launcher/resources/multimc/64x64/patreon.png new file mode 100644 index 0000000..ef5d690 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/patreon.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/refresh.png b/ultimmc/launcher/resources/multimc/64x64/refresh.png new file mode 100644 index 0000000..8373d81 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/refresh.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/resourcepacks.png b/ultimmc/launcher/resources/multimc/64x64/resourcepacks.png new file mode 100644 index 0000000..fb874e7 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/resourcepacks.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/screenshots.png b/ultimmc/launcher/resources/multimc/64x64/screenshots.png new file mode 100644 index 0000000..af18e39 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/screenshots.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/settings.png b/ultimmc/launcher/resources/multimc/64x64/settings.png new file mode 100644 index 0000000..e3ff58f Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/settings.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/star.png b/ultimmc/launcher/resources/multimc/64x64/star.png new file mode 100644 index 0000000..4ed5d97 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/star.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/status-bad.png b/ultimmc/launcher/resources/multimc/64x64/status-bad.png new file mode 100644 index 0000000..64060ba Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/status-bad.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/status-good.png b/ultimmc/launcher/resources/multimc/64x64/status-good.png new file mode 100644 index 0000000..e862ddc Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/status-good.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/status-running.png b/ultimmc/launcher/resources/multimc/64x64/status-running.png new file mode 100644 index 0000000..38afda0 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/status-running.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/status-yellow.png b/ultimmc/launcher/resources/multimc/64x64/status-yellow.png new file mode 100644 index 0000000..3d54d32 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/status-yellow.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/viewfolder.png b/ultimmc/launcher/resources/multimc/64x64/viewfolder.png new file mode 100644 index 0000000..7d531f9 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/viewfolder.png differ diff --git a/ultimmc/launcher/resources/multimc/64x64/worlds.png b/ultimmc/launcher/resources/multimc/64x64/worlds.png new file mode 100644 index 0000000..1d40f1d Binary files /dev/null and b/ultimmc/launcher/resources/multimc/64x64/worlds.png differ diff --git a/ultimmc/launcher/resources/multimc/8x8/noaccount.png b/ultimmc/launcher/resources/multimc/8x8/noaccount.png new file mode 100644 index 0000000..466e4c0 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/8x8/noaccount.png differ diff --git a/ultimmc/launcher/resources/multimc/index.theme b/ultimmc/launcher/resources/multimc/index.theme new file mode 100644 index 0000000..070e23f --- /dev/null +++ b/ultimmc/launcher/resources/multimc/index.theme @@ -0,0 +1,58 @@ +[Icon Theme] +Name=multimc +Comment=Default Icons +Inherits=default +Directories=8x8,16x16,22x22,24x24,32x32,32x32/instances,48x48,50x50/instances,64x64,128x128/instances,256x256,scalable,scalable/instances + +[8x8] +Size=8 + +[16x16] +Size=16 + +[22x22] +Size=22 + +[24x24] +Size=24 + +[32x32] +Size=32 + +[32x32/instances] +Size=32 +MinSize=1 +MaxSize=32 + +[48x48] +Size=48 + +[50x50/instances] +Size=50 + +[64x64] +Size=64 + +[128x128] +Size=128 +MinSize=33 +MaxSize=128 + +[128x128/instances] +Size=128 +MinSize=33 +MaxSize=128 + +[256x256] +Size=256 + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 + +[scalable/instances] +Size=128 +MinSize=16 +MaxSize=256 diff --git a/ultimmc/launcher/resources/multimc/multimc.qrc b/ultimmc/launcher/resources/multimc/multimc.qrc new file mode 100644 index 0000000..03894ee --- /dev/null +++ b/ultimmc/launcher/resources/multimc/multimc.qrc @@ -0,0 +1,321 @@ + + + + index.theme + + + scalable/reddit-alien.svg + + + 32x32/instances/flame.png + 128x128/instances/flame.png + + + scalable/launcher.svg + + + scalable/technic.svg + + + scalable/atlauncher.svg + scalable/atlauncher-placeholder.png + + + scalable/modrinth.svg + + + scalable/proxy.svg + + + scalable/language.svg + + + scalable/java.svg + + + 16x16/star.png + 24x24/star.png + 32x32/star.png + 48x48/star.png + 64x64/star.png + + + 16x16/worlds.png + 22x22/worlds.png + 32x32/worlds.png + 48x48/worlds.png + 64x64/worlds.png + + + 16x16/minecraft.png + 24x24/minecraft.png + 32x32/minecraft.png + 48x48/minecraft.png + 256x256/minecraft.png + + + 16x16/about.png + 22x22/about.png + 32x32/about.png + 48x48/about.png + 64x64/about.png + + + scalable/bug.svg + 16x16/bug.png + 22x22/bug.png + 32x32/bug.png + 48x48/bug.png + 64x64/bug.png + + + + 16x16/screenshots.png + 22x22/screenshots.png + 32x32/screenshots.png + 48x48/screenshots.png + 64x64/screenshots.png + scalable/screenshots.svg + + + scalable/custom-commands.svg + + + 16x16/patreon.png + 22x22/patreon.png + 24x24/patreon.png + 32x32/patreon.png + 48x48/patreon.png + 64x64/patreon.png + + + 16x16/cat.png + 22x22/cat.png + 24x24/cat.png + 32x32/cat.png + 48x48/cat.png + 64x64/cat.png + + + scalable/centralmods.svg + 16x16/centralmods.png + 22x22/centralmods.png + 32x32/centralmods.png + 48x48/centralmods.png + 64x64/centralmods.png + + + scalable/checkupdate.svg + 16x16/checkupdate.png + 22x22/checkupdate.png + 32x32/checkupdate.png + 48x48/checkupdate.png + 64x64/checkupdate.png + + + 16x16/copy.png + 22x22/copy.png + 32x32/copy.png + 48x48/copy.png + 64x64/copy.png + + + 16x16/help.png + 22x22/help.png + 32x32/help.png + 48x48/help.png + 64x64/help.png + + + 16x16/new.png + 22x22/new.png + 32x32/new.png + 48x48/new.png + 64x64/new.png + + + scalable/news.svg + 16x16/news.png + 22x22/news.png + 32x32/news.png + 48x48/news.png + 64x64/news.png + + + 16x16/status-bad.png + 24x24/status-bad.png + 22x22/status-bad.png + 32x32/status-bad.png + 48x48/status-bad.png + 64x64/status-bad.png + + + 16x16/status-good.png + 24x24/status-good.png + 22x22/status-good.png + 32x32/status-good.png + 48x48/status-good.png + 64x64/status-good.png + + + 16x16/status-yellow.png + 24x24/status-yellow.png + 22x22/status-yellow.png + 32x32/status-yellow.png + 48x48/status-yellow.png + 64x64/status-yellow.png + + + 16x16/status-running.png + 24x24/status-running.png + 22x22/status-running.png + 32x32/status-running.png + 48x48/status-running.png + 64x64/status-running.png + scalable/status-running.svg + + + 16x16/loadermods.png + 24x24/loadermods.png + 32x32/loadermods.png + 64x64/loadermods.png + + + 16x16/jarmods.png + 24x24/jarmods.png + 32x32/jarmods.png + 64x64/jarmods.png + + + 16x16/coremods.png + 24x24/coremods.png + 32x32/coremods.png + 64x64/coremods.png + + + 16x16/resourcepacks.png + 24x24/resourcepacks.png + 32x32/resourcepacks.png + 64x64/resourcepacks.png + + + 128x128/shaderpacks.png + + + 16x16/refresh.png + 22x22/refresh.png + 32x32/refresh.png + 48x48/refresh.png + 64x64/refresh.png + + + 16x16/settings.png + 22x22/settings.png + 32x32/settings.png + 48x48/settings.png + 64x64/settings.png + + + 16x16/instance-settings.png + 22x22/instance-settings.png + 32x32/instance-settings.png + 48x48/instance-settings.png + 64x64/instance-settings.png + + + scalable/viewfolder.svg + 16x16/viewfolder.png + 22x22/viewfolder.png + 32x32/viewfolder.png + 48x48/viewfolder.png + 64x64/viewfolder.png + + + 8x8/noaccount.png + 16x16/noaccount.png + 24x24/noaccount.png + 32x32/noaccount.png + 48x48/noaccount.png + + + 8x8/noaccount.png + 16x16/noaccount.png + 24x24/noaccount.png + 32x32/noaccount.png + 48x48/noaccount.png + + + 16x16/log.png + 24x24/log.png + 32x32/log.png + 48x48/log.png + 64x64/log.png + + + 128x128/unknown_server.png + + + scalable/screenshot-placeholder.svg + + + scalable/discord.svg + + + 32x32/instances/chicken.png + 128x128/instances/chicken.png + + 32x32/instances/creeper.png + 128x128/instances/creeper.png + + 32x32/instances/enderpearl.png + 128x128/instances/enderpearl.png + + 32x32/instances/ftb_glow.png + 128x128/instances/ftb_glow.png + + 32x32/instances/ftb_logo.png + 128x128/instances/ftb_logo.png + + 32x32/instances/flame.png + 128x128/instances/flame.png + + 32x32/instances/gear.png + 128x128/instances/gear.png + + 32x32/instances/herobrine.png + 128x128/instances/herobrine.png + + 32x32/instances/magitech.png + 128x128/instances/magitech.png + + 32x32/instances/meat.png + 128x128/instances/meat.png + + 32x32/instances/netherstar.png + 128x128/instances/netherstar.png + + 32x32/instances/skeleton.png + 128x128/instances/skeleton.png + + 32x32/instances/squarecreeper.png + 128x128/instances/squarecreeper.png + + 32x32/instances/steve.png + 128x128/instances/steve.png + + 32x32/instances/brick.png + 32x32/instances/diamond.png + 32x32/instances/dirt.png + 32x32/instances/gold.png + 32x32/instances/grass.png + 32x32/instances/iron.png + 32x32/instances/planks.png + 32x32/instances/stone.png + 32x32/instances/tnt.png + + 50x50/instances/enderman.png + + scalable/instances/fox.svg + scalable/instances/bee.svg + + diff --git a/ultimmc/launcher/resources/multimc/scalable/atlauncher-placeholder.png b/ultimmc/launcher/resources/multimc/scalable/atlauncher-placeholder.png new file mode 100644 index 0000000..f4314c4 Binary files /dev/null and b/ultimmc/launcher/resources/multimc/scalable/atlauncher-placeholder.png differ diff --git a/ultimmc/launcher/resources/multimc/scalable/atlauncher.svg b/ultimmc/launcher/resources/multimc/scalable/atlauncher.svg new file mode 100644 index 0000000..1bb5f35 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/atlauncher.svg @@ -0,0 +1,15 @@ + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/bug.svg b/ultimmc/launcher/resources/multimc/scalable/bug.svg new file mode 100644 index 0000000..178e3c2 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/bug.svg @@ -0,0 +1,387 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/centralmods.svg b/ultimmc/launcher/resources/multimc/scalable/centralmods.svg new file mode 100644 index 0000000..a8b123d --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/centralmods.svg @@ -0,0 +1,346 @@ + + + + + + + + + + unsorted + + + + + Open Clip Art Library, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons + + + + + + + + + + + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/checkupdate.svg b/ultimmc/launcher/resources/multimc/scalable/checkupdate.svg new file mode 100644 index 0000000..fc09cb4 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/checkupdate.svg @@ -0,0 +1,167 @@ + + + + + + + + + + unsorted + + + + + Open Clip Art Library, Source: GNOME-Colors, Source: GNOME-Colors, Source: GNOME-Colors, Source: GNOME-Colors, Source: GNOME-Colors + + + + + + + + + + + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/custom-commands.svg b/ultimmc/launcher/resources/multimc/scalable/custom-commands.svg new file mode 100644 index 0000000..b7f1a14 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/custom-commands.svg @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/discord.svg b/ultimmc/launcher/resources/multimc/scalable/discord.svg new file mode 100644 index 0000000..067be1e --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/discord.svg @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/instances/bee.svg b/ultimmc/launcher/resources/multimc/scalable/instances/bee.svg new file mode 100644 index 0000000..49f216c --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/instances/bee.svg @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/instances/fox.svg b/ultimmc/launcher/resources/multimc/scalable/instances/fox.svg new file mode 100644 index 0000000..fcf16b2 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/instances/fox.svg @@ -0,0 +1,290 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/java.svg b/ultimmc/launcher/resources/multimc/scalable/java.svg new file mode 100644 index 0000000..fd15e5c --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/java.svg @@ -0,0 +1,773 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/language.svg b/ultimmc/launcher/resources/multimc/scalable/language.svg new file mode 100644 index 0000000..968e353 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/language.svg @@ -0,0 +1,109 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/multimc/scalable/launcher.svg b/ultimmc/launcher/resources/multimc/scalable/launcher.svg new file mode 100644 index 0000000..42a056d --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/launcher.svg @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/modrinth.svg b/ultimmc/launcher/resources/multimc/scalable/modrinth.svg new file mode 100644 index 0000000..a40f0e7 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/modrinth.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/new.svg b/ultimmc/launcher/resources/multimc/scalable/new.svg new file mode 100644 index 0000000..c9cff35 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/new.svg @@ -0,0 +1,127 @@ + + + + + + New Document + + + + regular + plaintext + text + document + + + + + Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme, Source: GNOME Icon Theme + + + + + Jakub Steiner + + + + + Jakub Steiner + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/news.svg b/ultimmc/launcher/resources/multimc/scalable/news.svg new file mode 100644 index 0000000..67a370d --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/news.svg @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce convallis mauris ullamcorper mauris viverra molestie. Donec ultricies faucibus laoreet. Donec convallis congue neque consequat vehicula. Morbi condimentum tempor nulla et rhoncus. Etiam auctor, augue eu pharetra congue, elit justo lacinia risus, non lacinia est justo sed erat. Ut risus urna, viverra id interdum in, molestie non sem. Morbi leo orci, gravida auctor tempor vel, varius et enim. Nulla sem enim, ultricies vel laoreet ac, semper vel mauris. Ut adipiscing sapien sed leo pretium id vulputate erat gravida. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras tempor leo sit amet velit molestie commodo eget tincidunt leo. Cras dictum metus non ante pulvinar pellentesque. Morbi id elit ullamcorper mi vulputate lobortis. Cras ac vehicula felis. Phasellus dictum, tellus at molestie pellentesque, purus purus. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce convallis mauris ullamcorper mauris viverra molestie. Donec ultricies faucibus laoreet. Donec convallis congue neque consequat vehicula. Morbi condimentum tempor nulla et rhoncus. Etiam auctor, augue eu pharetra congue, elit justo lacinia risus, non lacinia est justo sed erat. Ut risus urna, + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/proxy.svg b/ultimmc/launcher/resources/multimc/scalable/proxy.svg new file mode 100644 index 0000000..55ee6f9 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/proxy.svg @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/reddit-alien.svg b/ultimmc/launcher/resources/multimc/scalable/reddit-alien.svg new file mode 100644 index 0000000..46061a5 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/reddit-alien.svg @@ -0,0 +1,189 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/multimc/scalable/screenshot-placeholder.svg b/ultimmc/launcher/resources/multimc/scalable/screenshot-placeholder.svg new file mode 100644 index 0000000..a7a2a3d --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/screenshot-placeholder.svg @@ -0,0 +1,86 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/screenshots.svg b/ultimmc/launcher/resources/multimc/scalable/screenshots.svg new file mode 100644 index 0000000..a3d4d8e --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/screenshots.svg @@ -0,0 +1,1231 @@ + + + + + Golden Picture Frame + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Open Clip Art Library + + + Golden Picture Frame + 2012-05-24T10:08:07 + Golden picture frame, Landscape + http://openclipart.org/detail/170182/golden-picture-frame-by-tasper + + + tasper + + + + + clip art + clipart + frame + golden + landscape + photo + picture + + + + + edited by Paul Sherman + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/status-bad.svg b/ultimmc/launcher/resources/multimc/scalable/status-bad.svg new file mode 100644 index 0000000..9f47307 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/status-bad.svg @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/status-good.svg b/ultimmc/launcher/resources/multimc/scalable/status-good.svg new file mode 100644 index 0000000..0a35c80 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/status-good.svg @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/status-running.svg b/ultimmc/launcher/resources/multimc/scalable/status-running.svg new file mode 100644 index 0000000..1820994 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/status-running.svg @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/status-yellow.svg b/ultimmc/launcher/resources/multimc/scalable/status-yellow.svg new file mode 100644 index 0000000..140e608 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/status-yellow.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/technic.svg b/ultimmc/launcher/resources/multimc/scalable/technic.svg new file mode 100644 index 0000000..91cbd3d --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/technic.svg @@ -0,0 +1,13 @@ + + + + + + image/svg+xml + + + + + + + diff --git a/ultimmc/launcher/resources/multimc/scalable/viewfolder.svg b/ultimmc/launcher/resources/multimc/scalable/viewfolder.svg new file mode 100644 index 0000000..4ba0ed0 --- /dev/null +++ b/ultimmc/launcher/resources/multimc/scalable/viewfolder.svg @@ -0,0 +1,122 @@ + + + + + + + + + + unsorted + + + + + Open Clip Art Library, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons, Source: Oxygen Icons + + + + + + + + + + + + + + image/svg+xml + + + en + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/index.theme b/ultimmc/launcher/resources/pe_blue/index.theme new file mode 100644 index 0000000..c9e0d93 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=pe_blue +Comment=Icons by pexner (blue) +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/ultimmc/launcher/resources/pe_blue/pe_blue.qrc b/ultimmc/launcher/resources/pe_blue/pe_blue.qrc new file mode 100644 index 0000000..c78d21a --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/pe_blue.qrc @@ -0,0 +1,39 @@ + + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/launcher.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/patreon.svg + scalable/proxy.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/shaderpacks.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/about.svg b/ultimmc/launcher/resources/pe_blue/scalable/about.svg new file mode 100644 index 0000000..56e7fc9 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/about.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/accounts.svg b/ultimmc/launcher/resources/pe_blue/scalable/accounts.svg new file mode 100644 index 0000000..77e3f45 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/accounts.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/bug.svg b/ultimmc/launcher/resources/pe_blue/scalable/bug.svg new file mode 100644 index 0000000..75a19e2 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/bug.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/centralmods.svg b/ultimmc/launcher/resources/pe_blue/scalable/centralmods.svg new file mode 100644 index 0000000..cda39b1 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/centralmods.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/checkupdate.svg b/ultimmc/launcher/resources/pe_blue/scalable/checkupdate.svg new file mode 100644 index 0000000..a7d9ee8 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/checkupdate.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/copy.svg b/ultimmc/launcher/resources/pe_blue/scalable/copy.svg new file mode 100644 index 0000000..7ce014e --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/copy.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/coremods.svg b/ultimmc/launcher/resources/pe_blue/scalable/coremods.svg new file mode 100644 index 0000000..4cc030d --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/coremods.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/custom-commands.svg b/ultimmc/launcher/resources/pe_blue/scalable/custom-commands.svg new file mode 100644 index 0000000..be76ece --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/custom-commands.svg @@ -0,0 +1,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/externaltools.svg b/ultimmc/launcher/resources/pe_blue/scalable/externaltools.svg new file mode 100644 index 0000000..45b7349 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/externaltools.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/help.svg b/ultimmc/launcher/resources/pe_blue/scalable/help.svg new file mode 100644 index 0000000..e98540c --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/help.svg @@ -0,0 +1,40 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/pe_blue/scalable/instance-settings.svg b/ultimmc/launcher/resources/pe_blue/scalable/instance-settings.svg new file mode 100644 index 0000000..43f0b2f --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/instance-settings.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/jarmods.svg b/ultimmc/launcher/resources/pe_blue/scalable/jarmods.svg new file mode 100644 index 0000000..bb75f4b --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/jarmods.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/java.svg b/ultimmc/launcher/resources/pe_blue/scalable/java.svg new file mode 100644 index 0000000..5e36920 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/java.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/language.svg b/ultimmc/launcher/resources/pe_blue/scalable/language.svg new file mode 100644 index 0000000..9286851 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/language.svg @@ -0,0 +1,46 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/pe_blue/scalable/launcher.svg b/ultimmc/launcher/resources/pe_blue/scalable/launcher.svg new file mode 100644 index 0000000..8b57737 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/launcher.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/loadermods.svg b/ultimmc/launcher/resources/pe_blue/scalable/loadermods.svg new file mode 100644 index 0000000..a54dc21 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/loadermods.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/log.svg b/ultimmc/launcher/resources/pe_blue/scalable/log.svg new file mode 100644 index 0000000..89d373f --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/log.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/minecraft.svg b/ultimmc/launcher/resources/pe_blue/scalable/minecraft.svg new file mode 100644 index 0000000..2fe6a02 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/minecraft.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/new.svg b/ultimmc/launcher/resources/pe_blue/scalable/new.svg new file mode 100644 index 0000000..dcc8579 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/new.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/news.svg b/ultimmc/launcher/resources/pe_blue/scalable/news.svg new file mode 100644 index 0000000..3ca3be3 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/news.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/notes.svg b/ultimmc/launcher/resources/pe_blue/scalable/notes.svg new file mode 100644 index 0000000..d099125 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/notes.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/patreon.svg b/ultimmc/launcher/resources/pe_blue/scalable/patreon.svg new file mode 100644 index 0000000..644b9b4 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/patreon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/proxy.svg b/ultimmc/launcher/resources/pe_blue/scalable/proxy.svg new file mode 100644 index 0000000..8266f9b --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/proxy.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/refresh.svg b/ultimmc/launcher/resources/pe_blue/scalable/refresh.svg new file mode 100644 index 0000000..a3d2281 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/refresh.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/resourcepacks.svg b/ultimmc/launcher/resources/pe_blue/scalable/resourcepacks.svg new file mode 100644 index 0000000..a17e7e8 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/resourcepacks.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/screenshots.svg b/ultimmc/launcher/resources/pe_blue/scalable/screenshots.svg new file mode 100644 index 0000000..1aa4e55 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/screenshots.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/settings.svg b/ultimmc/launcher/resources/pe_blue/scalable/settings.svg new file mode 100644 index 0000000..43f0b2f --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/settings.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/shaderpacks.svg b/ultimmc/launcher/resources/pe_blue/scalable/shaderpacks.svg new file mode 100644 index 0000000..530e1ad --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/shaderpacks.svg @@ -0,0 +1,77 @@ + +image/svg+xml diff --git a/ultimmc/launcher/resources/pe_blue/scalable/status-bad.svg b/ultimmc/launcher/resources/pe_blue/scalable/status-bad.svg new file mode 100644 index 0000000..4a48b5d --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/status-bad.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/status-good.svg b/ultimmc/launcher/resources/pe_blue/scalable/status-good.svg new file mode 100644 index 0000000..4cfa56f --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/status-good.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/status-yellow.svg b/ultimmc/launcher/resources/pe_blue/scalable/status-yellow.svg new file mode 100644 index 0000000..0551fed --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/status-yellow.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/viewfolder.svg b/ultimmc/launcher/resources/pe_blue/scalable/viewfolder.svg new file mode 100644 index 0000000..2634f8f --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/viewfolder.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_blue/scalable/worlds.svg b/ultimmc/launcher/resources/pe_blue/scalable/worlds.svg new file mode 100644 index 0000000..1670c03 --- /dev/null +++ b/ultimmc/launcher/resources/pe_blue/scalable/worlds.svg @@ -0,0 +1,63 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/pe_colored/index.theme b/ultimmc/launcher/resources/pe_colored/index.theme new file mode 100644 index 0000000..b757bbd --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=pe_colored +Comment=Icons by pexner (colored) +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/ultimmc/launcher/resources/pe_colored/pe_colored.qrc b/ultimmc/launcher/resources/pe_colored/pe_colored.qrc new file mode 100644 index 0000000..abe2cdb --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/pe_colored.qrc @@ -0,0 +1,39 @@ + + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/launcher.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/patreon.svg + scalable/proxy.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/shaderpacks.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/about.svg b/ultimmc/launcher/resources/pe_colored/scalable/about.svg new file mode 100644 index 0000000..95e9968 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/about.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/accounts.svg b/ultimmc/launcher/resources/pe_colored/scalable/accounts.svg new file mode 100644 index 0000000..301eb36 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/accounts.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/bug.svg b/ultimmc/launcher/resources/pe_colored/scalable/bug.svg new file mode 100644 index 0000000..8c92df0 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/bug.svg @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/centralmods.svg b/ultimmc/launcher/resources/pe_colored/scalable/centralmods.svg new file mode 100644 index 0000000..57a9725 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/centralmods.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/checkupdate.svg b/ultimmc/launcher/resources/pe_colored/scalable/checkupdate.svg new file mode 100644 index 0000000..0adc8ee --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/checkupdate.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/copy.svg b/ultimmc/launcher/resources/pe_colored/scalable/copy.svg new file mode 100644 index 0000000..b9b0f1b --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/copy.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/coremods.svg b/ultimmc/launcher/resources/pe_colored/scalable/coremods.svg new file mode 100644 index 0000000..ca7a22f --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/coremods.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/custom-commands.svg b/ultimmc/launcher/resources/pe_colored/scalable/custom-commands.svg new file mode 100644 index 0000000..44dd199 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/custom-commands.svg @@ -0,0 +1,345 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/externaltools.svg b/ultimmc/launcher/resources/pe_colored/scalable/externaltools.svg new file mode 100644 index 0000000..1469674 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/externaltools.svg @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/help.svg b/ultimmc/launcher/resources/pe_colored/scalable/help.svg new file mode 100644 index 0000000..c1ee525 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/help.svg @@ -0,0 +1,46 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/pe_colored/scalable/instance-settings.svg b/ultimmc/launcher/resources/pe_colored/scalable/instance-settings.svg new file mode 100644 index 0000000..72032f8 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/instance-settings.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/jarmods.svg b/ultimmc/launcher/resources/pe_colored/scalable/jarmods.svg new file mode 100644 index 0000000..bb75f4b --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/jarmods.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/java.svg b/ultimmc/launcher/resources/pe_colored/scalable/java.svg new file mode 100644 index 0000000..32c0225 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/java.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/language.svg b/ultimmc/launcher/resources/pe_colored/scalable/language.svg new file mode 100644 index 0000000..80c1dca --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/language.svg @@ -0,0 +1,44 @@ + +image/svg+xml + + + + + + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/pe_colored/scalable/launcher.svg b/ultimmc/launcher/resources/pe_colored/scalable/launcher.svg new file mode 100644 index 0000000..199b2da --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/launcher.svg @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/loadermods.svg b/ultimmc/launcher/resources/pe_colored/scalable/loadermods.svg new file mode 100644 index 0000000..2d80c7f --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/loadermods.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/log.svg b/ultimmc/launcher/resources/pe_colored/scalable/log.svg new file mode 100644 index 0000000..42659b5 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/log.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/minecraft.svg b/ultimmc/launcher/resources/pe_colored/scalable/minecraft.svg new file mode 100644 index 0000000..5281548 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/minecraft.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/new.svg b/ultimmc/launcher/resources/pe_colored/scalable/new.svg new file mode 100644 index 0000000..f18ed28 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/new.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/news.svg b/ultimmc/launcher/resources/pe_colored/scalable/news.svg new file mode 100644 index 0000000..4f924cd --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/news.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/notes.svg b/ultimmc/launcher/resources/pe_colored/scalable/notes.svg new file mode 100644 index 0000000..55ece16 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/notes.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/patreon.svg b/ultimmc/launcher/resources/pe_colored/scalable/patreon.svg new file mode 100644 index 0000000..d3c6d2d --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/patreon.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/proxy.svg b/ultimmc/launcher/resources/pe_colored/scalable/proxy.svg new file mode 100644 index 0000000..0aee69b --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/proxy.svg @@ -0,0 +1,15 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/refresh.svg b/ultimmc/launcher/resources/pe_colored/scalable/refresh.svg new file mode 100644 index 0000000..c2e7e91 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/refresh.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/resourcepacks.svg b/ultimmc/launcher/resources/pe_colored/scalable/resourcepacks.svg new file mode 100644 index 0000000..0318354 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/resourcepacks.svg @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/screenshots.svg b/ultimmc/launcher/resources/pe_colored/scalable/screenshots.svg new file mode 100644 index 0000000..844fcba --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/screenshots.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/settings.svg b/ultimmc/launcher/resources/pe_colored/scalable/settings.svg new file mode 100644 index 0000000..72032f8 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/settings.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/shaderpacks.svg b/ultimmc/launcher/resources/pe_colored/scalable/shaderpacks.svg new file mode 100644 index 0000000..9400b93 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/shaderpacks.svg @@ -0,0 +1,83 @@ + +image/svg+xml + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/status-bad.svg b/ultimmc/launcher/resources/pe_colored/scalable/status-bad.svg new file mode 100644 index 0000000..bc42c24 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/status-bad.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/status-good.svg b/ultimmc/launcher/resources/pe_colored/scalable/status-good.svg new file mode 100644 index 0000000..4cfa56f --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/status-good.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/status-yellow.svg b/ultimmc/launcher/resources/pe_colored/scalable/status-yellow.svg new file mode 100644 index 0000000..0551fed --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/status-yellow.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/viewfolder.svg b/ultimmc/launcher/resources/pe_colored/scalable/viewfolder.svg new file mode 100644 index 0000000..9183257 --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/viewfolder.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_colored/scalable/worlds.svg b/ultimmc/launcher/resources/pe_colored/scalable/worlds.svg new file mode 100644 index 0000000..087ba7c --- /dev/null +++ b/ultimmc/launcher/resources/pe_colored/scalable/worlds.svg @@ -0,0 +1,50 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/pe_dark/index.theme b/ultimmc/launcher/resources/pe_dark/index.theme new file mode 100644 index 0000000..b7d1ad0 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=pe_dark +Comment=Icons by pexner (dark) +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/ultimmc/launcher/resources/pe_dark/pe_dark.qrc b/ultimmc/launcher/resources/pe_dark/pe_dark.qrc new file mode 100644 index 0000000..03ae7ef --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/pe_dark.qrc @@ -0,0 +1,39 @@ + + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/launcher.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/patreon.svg + scalable/proxy.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/shaderpacks.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/about.svg b/ultimmc/launcher/resources/pe_dark/scalable/about.svg new file mode 100644 index 0000000..e75ea6c --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/about.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/accounts.svg b/ultimmc/launcher/resources/pe_dark/scalable/accounts.svg new file mode 100644 index 0000000..6d46b2d --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/accounts.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/bug.svg b/ultimmc/launcher/resources/pe_dark/scalable/bug.svg new file mode 100644 index 0000000..9da71ad --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/bug.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/centralmods.svg b/ultimmc/launcher/resources/pe_dark/scalable/centralmods.svg new file mode 100644 index 0000000..f3b0c0e --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/centralmods.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/checkupdate.svg b/ultimmc/launcher/resources/pe_dark/scalable/checkupdate.svg new file mode 100644 index 0000000..9758544 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/checkupdate.svg @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/copy.svg b/ultimmc/launcher/resources/pe_dark/scalable/copy.svg new file mode 100644 index 0000000..8c30ac0 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/copy.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/coremods.svg b/ultimmc/launcher/resources/pe_dark/scalable/coremods.svg new file mode 100644 index 0000000..1e2eb22 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/coremods.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/custom-commands.svg b/ultimmc/launcher/resources/pe_dark/scalable/custom-commands.svg new file mode 100644 index 0000000..42185e3 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/custom-commands.svg @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/externaltools.svg b/ultimmc/launcher/resources/pe_dark/scalable/externaltools.svg new file mode 100644 index 0000000..29b45f2 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/externaltools.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/help.svg b/ultimmc/launcher/resources/pe_dark/scalable/help.svg new file mode 100644 index 0000000..2a1518a --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/help.svg @@ -0,0 +1,34 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/pe_dark/scalable/instance-settings.svg b/ultimmc/launcher/resources/pe_dark/scalable/instance-settings.svg new file mode 100644 index 0000000..c9f701e --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/instance-settings.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/jarmods.svg b/ultimmc/launcher/resources/pe_dark/scalable/jarmods.svg new file mode 100644 index 0000000..cb9a97b --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/jarmods.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/java.svg b/ultimmc/launcher/resources/pe_dark/scalable/java.svg new file mode 100644 index 0000000..9e1091f --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/java.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/language.svg b/ultimmc/launcher/resources/pe_dark/scalable/language.svg new file mode 100644 index 0000000..1a9b4c5 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/language.svg @@ -0,0 +1,45 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/pe_dark/scalable/launcher.svg b/ultimmc/launcher/resources/pe_dark/scalable/launcher.svg new file mode 100644 index 0000000..346729f --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/launcher.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/loadermods.svg b/ultimmc/launcher/resources/pe_dark/scalable/loadermods.svg new file mode 100644 index 0000000..24226a0 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/loadermods.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/log.svg b/ultimmc/launcher/resources/pe_dark/scalable/log.svg new file mode 100644 index 0000000..68686a7 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/log.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/minecraft.svg b/ultimmc/launcher/resources/pe_dark/scalable/minecraft.svg new file mode 100644 index 0000000..01baf57 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/minecraft.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/new.svg b/ultimmc/launcher/resources/pe_dark/scalable/new.svg new file mode 100644 index 0000000..0377ace --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/new.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/news.svg b/ultimmc/launcher/resources/pe_dark/scalable/news.svg new file mode 100644 index 0000000..84979dc --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/news.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/notes.svg b/ultimmc/launcher/resources/pe_dark/scalable/notes.svg new file mode 100644 index 0000000..7264972 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/notes.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/patreon.svg b/ultimmc/launcher/resources/pe_dark/scalable/patreon.svg new file mode 100644 index 0000000..01cb279 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/patreon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/proxy.svg b/ultimmc/launcher/resources/pe_dark/scalable/proxy.svg new file mode 100644 index 0000000..98bcfac --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/proxy.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/refresh.svg b/ultimmc/launcher/resources/pe_dark/scalable/refresh.svg new file mode 100644 index 0000000..c227cd6 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/refresh.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/resourcepacks.svg b/ultimmc/launcher/resources/pe_dark/scalable/resourcepacks.svg new file mode 100644 index 0000000..0db2beb --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/resourcepacks.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/screenshots.svg b/ultimmc/launcher/resources/pe_dark/scalable/screenshots.svg new file mode 100644 index 0000000..2803b9a --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/screenshots.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/settings.svg b/ultimmc/launcher/resources/pe_dark/scalable/settings.svg new file mode 100644 index 0000000..c9f701e --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/settings.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/shaderpacks.svg b/ultimmc/launcher/resources/pe_dark/scalable/shaderpacks.svg new file mode 100644 index 0000000..9cca756 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/shaderpacks.svg @@ -0,0 +1,81 @@ + +image/svg+xml diff --git a/ultimmc/launcher/resources/pe_dark/scalable/status-bad.svg b/ultimmc/launcher/resources/pe_dark/scalable/status-bad.svg new file mode 100644 index 0000000..f455965 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/status-bad.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/status-good.svg b/ultimmc/launcher/resources/pe_dark/scalable/status-good.svg new file mode 100644 index 0000000..4ba91f2 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/status-good.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/status-yellow.svg b/ultimmc/launcher/resources/pe_dark/scalable/status-yellow.svg new file mode 100644 index 0000000..6913381 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/status-yellow.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/viewfolder.svg b/ultimmc/launcher/resources/pe_dark/scalable/viewfolder.svg new file mode 100644 index 0000000..3af3624 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/viewfolder.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_dark/scalable/worlds.svg b/ultimmc/launcher/resources/pe_dark/scalable/worlds.svg new file mode 100644 index 0000000..2b01070 --- /dev/null +++ b/ultimmc/launcher/resources/pe_dark/scalable/worlds.svg @@ -0,0 +1,63 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/pe_light/index.theme b/ultimmc/launcher/resources/pe_light/index.theme new file mode 100644 index 0000000..c106acc --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/index.theme @@ -0,0 +1,11 @@ +[Icon Theme] +Name=pe_light +Comment=Icons by pexner (light) +Inherits=multimc +Directories=scalable + +[scalable] +Size=48 +Type=Scalable +MinSize=16 +MaxSize=256 diff --git a/ultimmc/launcher/resources/pe_light/pe_light.qrc b/ultimmc/launcher/resources/pe_light/pe_light.qrc new file mode 100644 index 0000000..93d0020 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/pe_light.qrc @@ -0,0 +1,40 @@ + + + + index.theme + scalable/about.svg + scalable/accounts.svg + scalable/bug.svg + scalable/centralmods.svg + scalable/checkupdate.svg + scalable/copy.svg + scalable/coremods.svg + scalable/custom-commands.svg + scalable/externaltools.svg + scalable/help.svg + scalable/instance-settings.svg + scalable/jarmods.svg + scalable/java.svg + scalable/language.svg + scalable/launcher.svg + scalable/loadermods.svg + scalable/log.svg + scalable/minecraft.svg + scalable/new.svg + scalable/news.svg + scalable/notes.svg + scalable/patreon.svg + scalable/proxy.svg + scalable/refresh.svg + scalable/resourcepacks.svg + scalable/shaderpacks.svg + scalable/screenshots.svg + scalable/settings.svg + scalable/status-bad.svg + scalable/status-good.svg + scalable/status-yellow.svg + scalable/viewfolder.svg + scalable/worlds.svg + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/about.svg b/ultimmc/launcher/resources/pe_light/scalable/about.svg new file mode 100644 index 0000000..8d00c32 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/about.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/accounts.svg b/ultimmc/launcher/resources/pe_light/scalable/accounts.svg new file mode 100644 index 0000000..3a092d0 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/accounts.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/bug.svg b/ultimmc/launcher/resources/pe_light/scalable/bug.svg new file mode 100644 index 0000000..ccb64bc --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/bug.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/centralmods.svg b/ultimmc/launcher/resources/pe_light/scalable/centralmods.svg new file mode 100644 index 0000000..050fdc5 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/centralmods.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/checkupdate.svg b/ultimmc/launcher/resources/pe_light/scalable/checkupdate.svg new file mode 100644 index 0000000..08b8dcd --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/checkupdate.svg @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/copy.svg b/ultimmc/launcher/resources/pe_light/scalable/copy.svg new file mode 100644 index 0000000..abdcce0 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/copy.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/coremods.svg b/ultimmc/launcher/resources/pe_light/scalable/coremods.svg new file mode 100644 index 0000000..c8fb0eb --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/coremods.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/custom-commands.svg b/ultimmc/launcher/resources/pe_light/scalable/custom-commands.svg new file mode 100644 index 0000000..b3dfe12 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/custom-commands.svg @@ -0,0 +1,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/externaltools.svg b/ultimmc/launcher/resources/pe_light/scalable/externaltools.svg new file mode 100644 index 0000000..4d232bc --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/externaltools.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/help.svg b/ultimmc/launcher/resources/pe_light/scalable/help.svg new file mode 100644 index 0000000..f820c67 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/help.svg @@ -0,0 +1,36 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/pe_light/scalable/instance-settings.svg b/ultimmc/launcher/resources/pe_light/scalable/instance-settings.svg new file mode 100644 index 0000000..83b92a5 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/instance-settings.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/jarmods.svg b/ultimmc/launcher/resources/pe_light/scalable/jarmods.svg new file mode 100644 index 0000000..9852c80 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/jarmods.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/java.svg b/ultimmc/launcher/resources/pe_light/scalable/java.svg new file mode 100644 index 0000000..0584058 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/java.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/language.svg b/ultimmc/launcher/resources/pe_light/scalable/language.svg new file mode 100644 index 0000000..57d5e3d --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/language.svg @@ -0,0 +1,80 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ultimmc/launcher/resources/pe_light/scalable/launcher.svg b/ultimmc/launcher/resources/pe_light/scalable/launcher.svg new file mode 100644 index 0000000..6dbeab5 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/launcher.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/loadermods.svg b/ultimmc/launcher/resources/pe_light/scalable/loadermods.svg new file mode 100644 index 0000000..913c196 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/loadermods.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/log.svg b/ultimmc/launcher/resources/pe_light/scalable/log.svg new file mode 100644 index 0000000..82282ca --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/log.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/minecraft.svg b/ultimmc/launcher/resources/pe_light/scalable/minecraft.svg new file mode 100644 index 0000000..d772111 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/minecraft.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/new.svg b/ultimmc/launcher/resources/pe_light/scalable/new.svg new file mode 100644 index 0000000..96fd1f5 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/new.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/news.svg b/ultimmc/launcher/resources/pe_light/scalable/news.svg new file mode 100644 index 0000000..6f184af --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/news.svg @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/notes.svg b/ultimmc/launcher/resources/pe_light/scalable/notes.svg new file mode 100644 index 0000000..02dc11e --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/notes.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/patreon.svg b/ultimmc/launcher/resources/pe_light/scalable/patreon.svg new file mode 100644 index 0000000..0bd0882 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/patreon.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/proxy.svg b/ultimmc/launcher/resources/pe_light/scalable/proxy.svg new file mode 100644 index 0000000..9de8d6d --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/proxy.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/refresh.svg b/ultimmc/launcher/resources/pe_light/scalable/refresh.svg new file mode 100644 index 0000000..9a724d9 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/refresh.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/resourcepacks.svg b/ultimmc/launcher/resources/pe_light/scalable/resourcepacks.svg new file mode 100644 index 0000000..7d6323f --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/resourcepacks.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/screenshots.svg b/ultimmc/launcher/resources/pe_light/scalable/screenshots.svg new file mode 100644 index 0000000..f2887be --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/screenshots.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/settings.svg b/ultimmc/launcher/resources/pe_light/scalable/settings.svg new file mode 100644 index 0000000..83b92a5 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/settings.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/shaderpacks.svg b/ultimmc/launcher/resources/pe_light/scalable/shaderpacks.svg new file mode 100644 index 0000000..76356ee --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/shaderpacks.svg @@ -0,0 +1,81 @@ + +image/svg+xml diff --git a/ultimmc/launcher/resources/pe_light/scalable/status-bad.svg b/ultimmc/launcher/resources/pe_light/scalable/status-bad.svg new file mode 100644 index 0000000..2c24970 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/status-bad.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/status-good.svg b/ultimmc/launcher/resources/pe_light/scalable/status-good.svg new file mode 100644 index 0000000..bf9a417 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/status-good.svg @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/status-yellow.svg b/ultimmc/launcher/resources/pe_light/scalable/status-yellow.svg new file mode 100644 index 0000000..f7d2236 --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/status-yellow.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/viewfolder.svg b/ultimmc/launcher/resources/pe_light/scalable/viewfolder.svg new file mode 100644 index 0000000..b36343f --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/viewfolder.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/pe_light/scalable/worlds.svg b/ultimmc/launcher/resources/pe_light/scalable/worlds.svg new file mode 100644 index 0000000..bf4c21a --- /dev/null +++ b/ultimmc/launcher/resources/pe_light/scalable/worlds.svg @@ -0,0 +1,64 @@ + +image/svg+xml \ No newline at end of file diff --git a/ultimmc/launcher/resources/sources/burfcat_hat.png b/ultimmc/launcher/resources/sources/burfcat_hat.png new file mode 100644 index 0000000..a378c1f Binary files /dev/null and b/ultimmc/launcher/resources/sources/burfcat_hat.png differ diff --git a/ultimmc/launcher/resources/sources/cattiversary.xcf b/ultimmc/launcher/resources/sources/cattiversary.xcf new file mode 100644 index 0000000..0026cd3 Binary files /dev/null and b/ultimmc/launcher/resources/sources/cattiversary.xcf differ diff --git a/ultimmc/launcher/resources/sources/clucker.svg b/ultimmc/launcher/resources/sources/clucker.svg new file mode 100644 index 0000000..0c1727e --- /dev/null +++ b/ultimmc/launcher/resources/sources/clucker.svg @@ -0,0 +1,404 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/sources/creeper.svg b/ultimmc/launcher/resources/sources/creeper.svg new file mode 100644 index 0000000..2a2e39b --- /dev/null +++ b/ultimmc/launcher/resources/sources/creeper.svg @@ -0,0 +1,775 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/sources/enderpearl.svg b/ultimmc/launcher/resources/sources/enderpearl.svg new file mode 100644 index 0000000..ac9378f --- /dev/null +++ b/ultimmc/launcher/resources/sources/enderpearl.svg @@ -0,0 +1,271 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/sources/flame.svg b/ultimmc/launcher/resources/sources/flame.svg new file mode 100644 index 0000000..8a6da2f --- /dev/null +++ b/ultimmc/launcher/resources/sources/flame.svg @@ -0,0 +1,51 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/sources/ftb-glow.svg b/ultimmc/launcher/resources/sources/ftb-glow.svg new file mode 100644 index 0000000..be78c78 --- /dev/null +++ b/ultimmc/launcher/resources/sources/ftb-glow.svg @@ -0,0 +1,606 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/sources/ftb-logo.svg b/ultimmc/launcher/resources/sources/ftb-logo.svg new file mode 100644 index 0000000..8cf7356 --- /dev/null +++ b/ultimmc/launcher/resources/sources/ftb-logo.svg @@ -0,0 +1,257 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/sources/gear.svg b/ultimmc/launcher/resources/sources/gear.svg new file mode 100644 index 0000000..c0169ae --- /dev/null +++ b/ultimmc/launcher/resources/sources/gear.svg @@ -0,0 +1,434 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/sources/herobrine.svg b/ultimmc/launcher/resources/sources/herobrine.svg new file mode 100644 index 0000000..7392ba3 --- /dev/null +++ b/ultimmc/launcher/resources/sources/herobrine.svg @@ -0,0 +1,583 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/sources/magitech.svg b/ultimmc/launcher/resources/sources/magitech.svg new file mode 100644 index 0000000..c6dd6bc --- /dev/null +++ b/ultimmc/launcher/resources/sources/magitech.svg @@ -0,0 +1,886 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/sources/meat.svg b/ultimmc/launcher/resources/sources/meat.svg new file mode 100644 index 0000000..69a2007 --- /dev/null +++ b/ultimmc/launcher/resources/sources/meat.svg @@ -0,0 +1,371 @@ + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/sources/netherstar.svg b/ultimmc/launcher/resources/sources/netherstar.svg new file mode 100644 index 0000000..4046e4e --- /dev/null +++ b/ultimmc/launcher/resources/sources/netherstar.svg @@ -0,0 +1,342 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/sources/pskeleton.svg b/ultimmc/launcher/resources/sources/pskeleton.svg new file mode 100644 index 0000000..c2783df --- /dev/null +++ b/ultimmc/launcher/resources/sources/pskeleton.svg @@ -0,0 +1,581 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/sources/skeleton.svg b/ultimmc/launcher/resources/sources/skeleton.svg new file mode 100644 index 0000000..5d55f27 --- /dev/null +++ b/ultimmc/launcher/resources/sources/skeleton.svg @@ -0,0 +1,610 @@ + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/sources/squarecreeper.svg b/ultimmc/launcher/resources/sources/squarecreeper.svg new file mode 100644 index 0000000..a1b0f4d --- /dev/null +++ b/ultimmc/launcher/resources/sources/squarecreeper.svg @@ -0,0 +1,828 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/resources/sources/steve.svg b/ultimmc/launcher/resources/sources/steve.svg new file mode 100644 index 0000000..2233272 --- /dev/null +++ b/ultimmc/launcher/resources/sources/steve.svg @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/screenshots/ImgurAlbumCreation.cpp b/ultimmc/launcher/screenshots/ImgurAlbumCreation.cpp new file mode 100644 index 0000000..d5de302 --- /dev/null +++ b/ultimmc/launcher/screenshots/ImgurAlbumCreation.cpp @@ -0,0 +1,88 @@ +#include "ImgurAlbumCreation.h" + +#include +#include +#include +#include +#include +#include + +#include "BuildConfig.h" +#include "Application.h" + +ImgurAlbumCreation::ImgurAlbumCreation(QList screenshots) : NetAction(), m_screenshots(screenshots) +{ + m_url = BuildConfig.IMGUR_BASE_URL + "album.json"; + m_status = Job_NotStarted; +} + +void ImgurAlbumCreation::startImpl() +{ + m_status = Job_InProgress; + QNetworkRequest request(m_url); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); + request.setRawHeader("Accept", "application/json"); + + QStringList hashes; + for (auto shot : m_screenshots) + { + hashes.append(shot->m_imgurDeleteHash); + } + + const QByteArray data = "deletehashes=" + hashes.join(',').toUtf8() + "&title=Minecraft%20Screenshots&privacy=hidden"; + + QNetworkReply *rep = APPLICATION->network()->post(request, data); + + m_reply.reset(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &ImgurAlbumCreation::downloadProgress); + connect(rep, &QNetworkReply::finished, this, &ImgurAlbumCreation::downloadFinished); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(downloadError(QNetworkReply::NetworkError))); +} +void ImgurAlbumCreation::downloadError(QNetworkReply::NetworkError error) +{ + qDebug() << m_reply->errorString(); + m_status = Job_Failed; +} +void ImgurAlbumCreation::downloadFinished() +{ + if (m_status != Job_Failed) + { + QByteArray data = m_reply->readAll(); + m_reply.reset(); + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + qDebug() << jsonError.errorString(); + emit failed(m_index_within_job); + return; + } + auto object = doc.object(); + if (!object.value("success").toBool()) + { + qDebug() << doc.toJson(); + emit failed(m_index_within_job); + return; + } + m_deleteHash = object.value("data").toObject().value("deletehash").toString(); + m_id = object.value("data").toObject().value("id").toString(); + m_status = Job_Finished; + emit succeeded(m_index_within_job); + return; + } + else + { + qDebug() << m_reply->readAll(); + m_reply.reset(); + emit failed(m_index_within_job); + return; + } +} +void ImgurAlbumCreation::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + m_total_progress = bytesTotal; + m_progress = bytesReceived; + emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); +} diff --git a/ultimmc/launcher/screenshots/ImgurAlbumCreation.h b/ultimmc/launcher/screenshots/ImgurAlbumCreation.h new file mode 100644 index 0000000..cb048a2 --- /dev/null +++ b/ultimmc/launcher/screenshots/ImgurAlbumCreation.h @@ -0,0 +1,43 @@ +#pragma once +#include "net/NetAction.h" +#include "Screenshot.h" +#include "QObjectPtr.h" + +typedef shared_qobject_ptr ImgurAlbumCreationPtr; +class ImgurAlbumCreation : public NetAction +{ +public: + explicit ImgurAlbumCreation(QList screenshots); + static ImgurAlbumCreationPtr make(QList screenshots) + { + return ImgurAlbumCreationPtr(new ImgurAlbumCreation(screenshots)); + } + + QString deleteHash() const + { + return m_deleteHash; + } + QString id() const + { + return m_id; + } + +protected +slots: + virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); + virtual void downloadError(QNetworkReply::NetworkError error); + virtual void downloadFinished(); + virtual void downloadReadyRead() + { + } + +public +slots: + virtual void startImpl(); + +private: + QList m_screenshots; + + QString m_deleteHash; + QString m_id; +}; diff --git a/ultimmc/launcher/screenshots/ImgurUpload.cpp b/ultimmc/launcher/screenshots/ImgurUpload.cpp new file mode 100644 index 0000000..76a8494 --- /dev/null +++ b/ultimmc/launcher/screenshots/ImgurUpload.cpp @@ -0,0 +1,112 @@ +#include "ImgurUpload.h" +#include "BuildConfig.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +ImgurUpload::ImgurUpload(ScreenShot::Ptr shot) : NetAction(), m_shot(shot) +{ + m_url = BuildConfig.IMGUR_BASE_URL + "upload.json"; + m_status = Job_NotStarted; +} + +void ImgurUpload::startImpl() +{ + finished = false; + m_status = Job_InProgress; + QNetworkRequest request(m_url); + request.setHeader(QNetworkRequest::UserAgentHeader, BuildConfig.USER_AGENT_UNCACHED); + request.setRawHeader("Authorization", QString("Client-ID %1").arg(BuildConfig.IMGUR_CLIENT_ID).toStdString().c_str()); + request.setRawHeader("Accept", "application/json"); + + QFile f(m_shot->m_file.absoluteFilePath()); + if (!f.open(QFile::ReadOnly)) + { + emit failed(m_index_within_job); + return; + } + + QHttpMultiPart *multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType); + QHttpPart filePart; + filePart.setBody(f.readAll().toBase64()); + filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/png"); + filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"image\""); + multipart->append(filePart); + QHttpPart typePart; + typePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"type\""); + typePart.setBody("base64"); + multipart->append(typePart); + QHttpPart namePart; + namePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"name\""); + namePart.setBody(m_shot->m_file.baseName().toUtf8()); + multipart->append(namePart); + + QNetworkReply *rep = m_network->post(request, multipart); + + m_reply.reset(rep); + connect(rep, &QNetworkReply::uploadProgress, this, &ImgurUpload::downloadProgress); + connect(rep, &QNetworkReply::finished, this, &ImgurUpload::downloadFinished); + connect(rep, SIGNAL(error(QNetworkReply::NetworkError)), + SLOT(downloadError(QNetworkReply::NetworkError))); +} +void ImgurUpload::downloadError(QNetworkReply::NetworkError error) +{ + qCritical() << "ImgurUpload failed with error" << m_reply->errorString() << "Server reply:\n" << m_reply->readAll(); + if(finished) + { + qCritical() << "Double finished ImgurUpload!"; + return; + } + m_status = Job_Failed; + finished = true; + m_reply.reset(); + emit failed(m_index_within_job); +} +void ImgurUpload::downloadFinished() +{ + if(finished) + { + qCritical() << "Double finished ImgurUpload!"; + return; + } + QByteArray data = m_reply->readAll(); + m_reply.reset(); + QJsonParseError jsonError; + QJsonDocument doc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + qDebug() << "imgur server did not reply with JSON" << jsonError.errorString(); + finished = true; + m_reply.reset(); + emit failed(m_index_within_job); + return; + } + auto object = doc.object(); + if (!object.value("success").toBool()) + { + qDebug() << "Screenshot upload not successful:" << doc.toJson(); + finished = true; + m_reply.reset(); + emit failed(m_index_within_job); + return; + } + m_shot->m_imgurId = object.value("data").toObject().value("id").toString(); + m_shot->m_url = object.value("data").toObject().value("link").toString(); + m_shot->m_imgurDeleteHash = object.value("data").toObject().value("deletehash").toString(); + m_status = Job_Finished; + finished = true; + emit succeeded(m_index_within_job); + return; +} +void ImgurUpload::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + m_total_progress = bytesTotal; + m_progress = bytesReceived; + emit netActionProgress(m_index_within_job, bytesReceived, bytesTotal); +} diff --git a/ultimmc/launcher/screenshots/ImgurUpload.h b/ultimmc/launcher/screenshots/ImgurUpload.h new file mode 100644 index 0000000..cf54f58 --- /dev/null +++ b/ultimmc/launcher/screenshots/ImgurUpload.h @@ -0,0 +1,29 @@ +#pragma once +#include "QObjectPtr.h" +#include "net/NetAction.h" +#include "Screenshot.h" + +class ImgurUpload : public NetAction { +public: + using Ptr = shared_qobject_ptr; + + explicit ImgurUpload(ScreenShot::Ptr shot); + static Ptr make(ScreenShot::Ptr shot) { + return Ptr(new ImgurUpload(shot)); + } + +protected +slots: + void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override; + void downloadError(QNetworkReply::NetworkError error) override; + void downloadFinished() override; + void downloadReadyRead() override {} + +public +slots: + void startImpl() override; + +private: + ScreenShot::Ptr m_shot; + bool finished = true; +}; diff --git a/ultimmc/launcher/screenshots/Screenshot.h b/ultimmc/launcher/screenshots/Screenshot.h new file mode 100644 index 0000000..ca45aab --- /dev/null +++ b/ultimmc/launcher/screenshots/Screenshot.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include +#include + +struct ScreenShot { + using Ptr = std::shared_ptr; + + ScreenShot(QFileInfo file) { + m_file = file; + } + QFileInfo m_file; + QString m_url; + QString m_imgurId; + QString m_imgurDeleteHash; +}; diff --git a/ultimmc/launcher/settings/INIFile.cpp b/ultimmc/launcher/settings/INIFile.cpp new file mode 100644 index 0000000..6a3c801 --- /dev/null +++ b/ultimmc/launcher/settings/INIFile.cpp @@ -0,0 +1,163 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "settings/INIFile.h" +#include + +#include +#include +#include +#include +#include + +INIFile::INIFile() +{ +} + +QString INIFile::unescape(QString orig) +{ + QString out; + QChar prev = 0; + for(auto c: orig) + { + if(prev == '\\') + { + if(c == 'n') + out += '\n'; + else if(c == 't') + out += '\t'; + else if(c == '#') + out += '#'; + else + out += c; + prev = 0; + } + else + { + if(c == '\\') + { + prev = c; + continue; + } + out += c; + prev = 0; + } + } + return out; +} + +QString INIFile::escape(QString orig) +{ + QString out; + for(auto c: orig) + { + if(c == '\n') + out += "\\n"; + else if (c == '\t') + out += "\\t"; + else if(c == '\\') + out += "\\\\"; + else if(c == '#') + out += "\\#"; + else + out += c; + } + return out; +} + +bool INIFile::saveFile(QString fileName) +{ + QByteArray outArray; + for (Iterator iter = begin(); iter != end(); iter++) + { + QString value = iter.value().toString(); + value = escape(value); + outArray.append(iter.key().toUtf8()); + outArray.append('='); + outArray.append(value.toUtf8()); + outArray.append('\n'); + } + + try + { + FS::write(fileName, outArray); + } + catch (const Exception &e) + { + qCritical() << e.what(); + return false; + } + + return true; +} + + +bool INIFile::loadFile(QString fileName) +{ + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly)) + return false; + bool success = loadFile(file.readAll()); + file.close(); + return success; +} + +bool INIFile::loadFile(QByteArray file) +{ + QTextStream in(file); + in.setCodec("UTF-8"); + + QStringList lines = in.readAll().split('\n'); + for (int i = 0; i < lines.count(); i++) + { + QString &lineRaw = lines[i]; + // Ignore comments. + int commentIndex = 0; + QString line = lineRaw; + // Search for comments until no more escaped # are available + while((commentIndex = line.indexOf('#', commentIndex + 1)) != -1) { + if(commentIndex > 0 && line.at(commentIndex - 1) == '\\') { + continue; + } + line = line.left(lineRaw.indexOf('#')).trimmed(); + } + + int eqPos = line.indexOf('='); + if (eqPos == -1) + continue; + QString key = line.left(eqPos).trimmed(); + QString valueStr = line.right(line.length() - eqPos - 1).trimmed(); + + valueStr = unescape(valueStr); + + QVariant value(valueStr); + this->operator[](key) = value; + } + + return true; +} + +QVariant INIFile::get(QString key, QVariant def) const +{ + if (!this->contains(key)) + return def; + else + return this->operator[](key); +} + +void INIFile::set(QString key, QVariant val) +{ + this->operator[](key) = val; +} diff --git a/ultimmc/launcher/settings/INIFile.h b/ultimmc/launcher/settings/INIFile.h new file mode 100644 index 0000000..4313e82 --- /dev/null +++ b/ultimmc/launcher/settings/INIFile.h @@ -0,0 +1,36 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +// Sectionless INI parser (for instance config files) +class INIFile : public QMap +{ +public: + explicit INIFile(); + + bool loadFile(QByteArray file); + bool loadFile(QString fileName); + bool saveFile(QString fileName); + + QVariant get(QString key, QVariant def) const; + void set(QString key, QVariant val); + static QString unescape(QString orig); + static QString escape(QString orig); +}; diff --git a/ultimmc/launcher/settings/INIFile_test.cpp b/ultimmc/launcher/settings/INIFile_test.cpp new file mode 100644 index 0000000..08c2155 --- /dev/null +++ b/ultimmc/launcher/settings/INIFile_test.cpp @@ -0,0 +1,63 @@ +#include +#include "TestUtil.h" + +#include "settings/INIFile.h" + +class IniFileTest : public QObject +{ + Q_OBJECT +private +slots: + void initTestCase() + { + + } + void cleanupTestCase() + { + + } + + void test_Escape_data() + { + QTest::addColumn("through"); + + QTest::newRow("unix path") << "/abc/def/ghi/jkl"; + QTest::newRow("windows path") << "C:\\Program files\\terrible\\name\\of something\\"; + QTest::newRow("Plain text") << "Lorem ipsum dolor sit amet."; + QTest::newRow("Escape sequences") << "Lorem\n\t\n\\n\\tAAZ\nipsum dolor\n\nsit amet."; + QTest::newRow("Escape sequences 2") << "\"\n\n\""; + QTest::newRow("Hashtags") << "some data#something"; + } + void test_Escape() + { + QFETCH(QString, through); + + QString there = INIFile::escape(through); + QString back = INIFile::unescape(there); + + QCOMPARE(back, through); + } + + void test_SaveLoad() + { + QString a = "a"; + QString b = "a\nb\t\n\\\\\\C:\\Program files\\terrible\\name\\of something\\#thisIsNotAComment"; + QString filename = "test_SaveLoad.ini"; + + // save + INIFile f; + f.set("a", a); + f.set("b", b); + f.saveFile(filename); + + // load + INIFile f2; + f2.loadFile(filename); + QCOMPARE(a, f2.get("a","NOT SET").toString()); + QCOMPARE(b, f2.get("b","NOT SET").toString()); + } +}; + +QTEST_GUILESS_MAIN(IniFileTest) + +#include "INIFile_test.moc" diff --git a/ultimmc/launcher/settings/INISettingsObject.cpp b/ultimmc/launcher/settings/INISettingsObject.cpp new file mode 100644 index 0000000..1251340 --- /dev/null +++ b/ultimmc/launcher/settings/INISettingsObject.cpp @@ -0,0 +1,107 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "INISettingsObject.h" +#include "Setting.h" + +INISettingsObject::INISettingsObject(const QString &path, QObject *parent) + : SettingsObject(parent) +{ + m_filePath = path; + m_ini.loadFile(path); +} + +void INISettingsObject::setFilePath(const QString &filePath) +{ + m_filePath = filePath; +} + +bool INISettingsObject::reload() +{ + return m_ini.loadFile(m_filePath) && SettingsObject::reload(); +} + +void INISettingsObject::suspendSave() +{ + m_suspendSave = true; +} + +void INISettingsObject::resumeSave() +{ + m_suspendSave = false; + if(m_doSave) + { + m_ini.saveFile(m_filePath); + } +} + +void INISettingsObject::changeSetting(const Setting &setting, QVariant value) +{ + if (contains(setting.id())) + { + // valid value -> set the main config, remove all the sysnonyms + if (value.isValid()) + { + auto list = setting.configKeys(); + m_ini.set(list.takeFirst(), value); + for(auto iter: list) + m_ini.remove(iter); + } + // invalid -> remove all (just like resetSetting) + else + { + for(auto iter: setting.configKeys()) + m_ini.remove(iter); + } + doSave(); + } +} + +void INISettingsObject::doSave() +{ + if(m_suspendSave) + { + m_doSave = true; + } + else + { + m_ini.saveFile(m_filePath); + } +} + +void INISettingsObject::resetSetting(const Setting &setting) +{ + // if we have the setting, remove all the synonyms. ALL OF THEM + if (contains(setting.id())) + { + for(auto iter: setting.configKeys()) + m_ini.remove(iter); + doSave(); + } +} + +QVariant INISettingsObject::retrieveValue(const Setting &setting) +{ + // if we have the setting, return value of the first matching synonym + if (contains(setting.id())) + { + for(auto iter: setting.configKeys()) + { + if(m_ini.contains(iter)) + return m_ini[iter]; + } + } + return QVariant(); +} diff --git a/ultimmc/launcher/settings/INISettingsObject.h b/ultimmc/launcher/settings/INISettingsObject.h new file mode 100644 index 0000000..26cc32e --- /dev/null +++ b/ultimmc/launcher/settings/INISettingsObject.h @@ -0,0 +1,64 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "settings/INIFile.h" + +#include "settings/SettingsObject.h" + +/*! + * \brief A settings object that stores its settings in an INIFile. + */ +class INISettingsObject : public SettingsObject +{ + Q_OBJECT +public: + explicit INISettingsObject(const QString &path, QObject *parent = 0); + + /*! + * \brief Gets the path to the INI file. + * \return The path to the INI file. + */ + virtual QString filePath() const + { + return m_filePath; + } + + /*! + * \brief Sets the path to the INI file and reloads it. + * \param filePath The INI file's new path. + */ + virtual void setFilePath(const QString &filePath); + + bool reload() override; + + void suspendSave() override; + void resumeSave() override; + +protected slots: + virtual void changeSetting(const Setting &setting, QVariant value) override; + virtual void resetSetting(const Setting &setting) override; + +protected: + virtual QVariant retrieveValue(const Setting &setting) override; + void doSave(); + +protected: + INIFile m_ini; + QString m_filePath; +}; diff --git a/ultimmc/launcher/settings/OverrideSetting.cpp b/ultimmc/launcher/settings/OverrideSetting.cpp new file mode 100644 index 0000000..4396a38 --- /dev/null +++ b/ultimmc/launcher/settings/OverrideSetting.cpp @@ -0,0 +1,54 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "OverrideSetting.h" + +OverrideSetting::OverrideSetting(std::shared_ptr other, std::shared_ptr gate) + : Setting(other->configKeys(), QVariant()) +{ + Q_ASSERT(other); + Q_ASSERT(gate); + m_other = other; + m_gate = gate; +} + +bool OverrideSetting::isOverriding() const +{ + return m_gate->get().toBool(); +} + +QVariant OverrideSetting::defValue() const +{ + return m_other->get(); +} + +QVariant OverrideSetting::get() const +{ + if(isOverriding()) + { + return Setting::get(); + } + return m_other->get(); +} + +void OverrideSetting::reset() +{ + Setting::reset(); +} + +void OverrideSetting::set(QVariant value) +{ + Setting::set(value); +} diff --git a/ultimmc/launcher/settings/OverrideSetting.h b/ultimmc/launcher/settings/OverrideSetting.h new file mode 100644 index 0000000..9f0c98b --- /dev/null +++ b/ultimmc/launcher/settings/OverrideSetting.h @@ -0,0 +1,46 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "Setting.h" + +/*! + * \brief A setting that 'overrides another.' + * This means that the setting's default value will be the value of another setting. + * The other setting can be (and usually is) a part of a different SettingsObject + * than this one. + */ +class OverrideSetting : public Setting +{ + Q_OBJECT +public: + explicit OverrideSetting(std::shared_ptr overriden, std::shared_ptr gate); + + virtual QVariant defValue() const; + virtual QVariant get() const; + virtual void set (QVariant value); + virtual void reset(); + +private: + bool isOverriding() const; + +protected: + std::shared_ptr m_other; + std::shared_ptr m_gate; +}; diff --git a/ultimmc/launcher/settings/PassthroughSetting.cpp b/ultimmc/launcher/settings/PassthroughSetting.cpp new file mode 100644 index 0000000..8f93b25 --- /dev/null +++ b/ultimmc/launcher/settings/PassthroughSetting.cpp @@ -0,0 +1,69 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PassthroughSetting.h" + +PassthroughSetting::PassthroughSetting(std::shared_ptr other, std::shared_ptr gate) + : Setting(other->configKeys(), QVariant()) +{ + Q_ASSERT(other); + m_other = other; + m_gate = gate; +} + +bool PassthroughSetting::isOverriding() const +{ + if(!m_gate) + { + return false; + } + return m_gate->get().toBool(); +} + +QVariant PassthroughSetting::defValue() const +{ + if(isOverriding()) + { + return m_other->get(); + } + return m_other->defValue(); +} + +QVariant PassthroughSetting::get() const +{ + if(isOverriding()) + { + return Setting::get(); + } + return m_other->get(); +} + +void PassthroughSetting::reset() +{ + if(isOverriding()) + { + Setting::reset(); + } + m_other->reset(); +} + +void PassthroughSetting::set(QVariant value) +{ + if(isOverriding()) + { + Setting::set(value); + } + m_other->set(value); +} diff --git a/ultimmc/launcher/settings/PassthroughSetting.h b/ultimmc/launcher/settings/PassthroughSetting.h new file mode 100644 index 0000000..22008f8 --- /dev/null +++ b/ultimmc/launcher/settings/PassthroughSetting.h @@ -0,0 +1,45 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "Setting.h" + +/*! + * \brief A setting that 'overrides another.' based on the value of a 'gate' setting + * If 'gate' evaluates to true, the override stores and returns data + * If 'gate' evaluates to false, the original does, + */ +class PassthroughSetting : public Setting +{ + Q_OBJECT +public: + explicit PassthroughSetting(std::shared_ptr overriden, std::shared_ptr gate); + + virtual QVariant defValue() const; + virtual QVariant get() const; + virtual void set (QVariant value); + virtual void reset(); + +private: + bool isOverriding() const; + +protected: + std::shared_ptr m_other; + std::shared_ptr m_gate; +}; diff --git a/ultimmc/launcher/settings/Setting.cpp b/ultimmc/launcher/settings/Setting.cpp new file mode 100644 index 0000000..cfe5a7f --- /dev/null +++ b/ultimmc/launcher/settings/Setting.cpp @@ -0,0 +1,53 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Setting.h" +#include "settings/SettingsObject.h" + +Setting::Setting(QStringList synonyms, QVariant defVal) + : QObject(), m_synonyms(synonyms), m_defVal(defVal) +{ +} + +QVariant Setting::get() const +{ + SettingsObject *sbase = m_storage; + if (!sbase) + { + return defValue(); + } + else + { + QVariant test = sbase->retrieveValue(*this); + if (!test.isValid()) + return defValue(); + return test; + } +} + +QVariant Setting::defValue() const +{ + return m_defVal; +} + +void Setting::set(QVariant value) +{ + emit SettingChanged(*this, value); +} + +void Setting::reset() +{ + emit settingReset(*this); +} diff --git a/ultimmc/launcher/settings/Setting.h b/ultimmc/launcher/settings/Setting.h new file mode 100644 index 0000000..9beeb35 --- /dev/null +++ b/ultimmc/launcher/settings/Setting.h @@ -0,0 +1,117 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +class SettingsObject; + +/*! + * + */ +class Setting : public QObject +{ + Q_OBJECT +public: + /** + * Construct a Setting + * + * Synonyms are all the possible names used in the settings object, in order of preference. + * First synonym is the ID, which identifies the setting in MultiMC. + * + * defVal is the default value that will be returned when the settings object + * doesn't have any value for this setting. + */ + explicit Setting(QStringList synonyms, QVariant defVal = QVariant()); + + /*! + * \brief Gets this setting's ID. + * This is used to refer to the setting within the application. + * \warning Changing the ID while the setting is registered with a SettingsObject results in + * undefined behavior. + * \return The ID of the setting. + */ + virtual QString id() const + { + return m_synonyms.first(); + } + + /*! + * \brief Gets this setting's config file key. + * This is used to store the setting's value in the config file. It is usually + * the same as the setting's ID, but it can be different. + * \return The setting's config file key. + */ + virtual QStringList configKeys() const + { + return m_synonyms; + } + + /*! + * \brief Gets this setting's value as a QVariant. + * This is done by calling the SettingsObject's retrieveValue() function. + * If this Setting doesn't have a SettingsObject, this returns an invalid QVariant. + * \return QVariant containing this setting's value. + * \sa value() + */ + virtual QVariant get() const; + + /*! + * \brief Gets this setting's default value. + * \return The default value of this setting. + */ + virtual QVariant defValue() const; + +signals: + /*! + * \brief Signal emitted when this Setting object's value changes. + * \param setting A reference to the Setting that changed. + * \param value This Setting object's new value. + */ + void SettingChanged(const Setting &setting, QVariant value); + + /*! + * \brief Signal emitted when this Setting object's value resets to default. + * \param setting A reference to the Setting that changed. + */ + void settingReset(const Setting &setting); + +public +slots: + /*! + * \brief Changes the setting's value. + * This is done by emitting the SettingChanged() signal which will then be + * handled by the SettingsObject object and cause the setting to change. + * \param value The new value. + */ + virtual void set(QVariant value); + + /*! + * \brief Reset the setting to default + * This is done by emitting the settingReset() signal which will then be + * handled by the SettingsObject object and cause the setting to change. + */ + virtual void reset(); + +protected: + friend class SettingsObject; + SettingsObject * m_storage; + QStringList m_synonyms; + QVariant m_defVal; +}; diff --git a/ultimmc/launcher/settings/SettingsObject.cpp b/ultimmc/launcher/settings/SettingsObject.cpp new file mode 100644 index 0000000..8a0bc04 --- /dev/null +++ b/ultimmc/launcher/settings/SettingsObject.cpp @@ -0,0 +1,142 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "settings/SettingsObject.h" +#include "settings/Setting.h" +#include "settings/OverrideSetting.h" +#include "PassthroughSetting.h" +#include + +#include + +SettingsObject::SettingsObject(QObject *parent) : QObject(parent) +{ +} + +SettingsObject::~SettingsObject() +{ + m_settings.clear(); +} + +std::shared_ptr SettingsObject::registerOverride(std::shared_ptr original, + std::shared_ptr gate) +{ + if (contains(original->id())) + { + qCritical() << QString("Failed to register setting %1. ID already exists.") + .arg(original->id()); + return nullptr; // Fail + } + auto override = std::make_shared(original, gate); + override->m_storage = this; + connectSignals(*override); + m_settings.insert(override->id(), override); + return override; +} + +std::shared_ptr SettingsObject::registerPassthrough(std::shared_ptr original, + std::shared_ptr gate) +{ + if (contains(original->id())) + { + qCritical() << QString("Failed to register setting %1. ID already exists.") + .arg(original->id()); + return nullptr; // Fail + } + auto passthrough = std::make_shared(original, gate); + passthrough->m_storage = this; + connectSignals(*passthrough); + m_settings.insert(passthrough->id(), passthrough); + return passthrough; +} + +std::shared_ptr SettingsObject::registerSetting(QStringList synonyms, QVariant defVal) +{ + if (synonyms.empty()) + return nullptr; + if (contains(synonyms.first())) + { + qCritical() << QString("Failed to register setting %1. ID already exists.") + .arg(synonyms.first()); + return nullptr; // Fail + } + auto setting = std::make_shared(synonyms, defVal); + setting->m_storage = this; + connectSignals(*setting); + m_settings.insert(setting->id(), setting); + return setting; +} + +std::shared_ptr SettingsObject::getSetting(const QString &id) const +{ + // Make sure there is a setting with the given ID. + if (!m_settings.contains(id)) + return NULL; + + return m_settings[id]; +} + +QVariant SettingsObject::get(const QString &id) const +{ + auto setting = getSetting(id); + return (setting ? setting->get() : QVariant()); +} + +bool SettingsObject::set(const QString &id, QVariant value) +{ + auto setting = getSetting(id); + if (!setting) + { + qCritical() << QString("Error changing setting %1. Setting doesn't exist.").arg(id); + return false; + } + else + { + setting->set(value); + return true; + } +} + +void SettingsObject::reset(const QString &id) const +{ + auto setting = getSetting(id); + if (setting) + setting->reset(); +} + +bool SettingsObject::contains(const QString &id) +{ + return m_settings.contains(id); +} + +bool SettingsObject::reload() +{ + for (auto setting : m_settings.values()) + { + setting->set(setting->get()); + } + return true; +} + +void SettingsObject::connectSignals(const Setting &setting) +{ + connect(&setting, SIGNAL(SettingChanged(const Setting &, QVariant)), + SLOT(changeSetting(const Setting &, QVariant))); + connect(&setting, SIGNAL(SettingChanged(const Setting &, QVariant)), + SIGNAL(SettingChanged(const Setting &, QVariant))); + + connect(&setting, SIGNAL(settingReset(Setting)), SLOT(resetSetting(const Setting &))); + connect(&setting, SIGNAL(settingReset(Setting)), SIGNAL(settingReset(const Setting &))); +} diff --git a/ultimmc/launcher/settings/SettingsObject.h b/ultimmc/launcher/settings/SettingsObject.h new file mode 100644 index 0000000..3d61e70 --- /dev/null +++ b/ultimmc/launcher/settings/SettingsObject.h @@ -0,0 +1,212 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include + +class Setting; +class SettingsObject; + +typedef std::shared_ptr SettingsObjectPtr; + +/*! + * \brief The SettingsObject handles communicating settings between the application and a + *settings file. + * The class keeps a list of Setting objects. Each Setting object represents one + * of the application's settings. These Setting objects are registered with + * a SettingsObject and can be managed similarly to the way a list works. + * + * \author Andrew Okin + * \date 2/22/2013 + * + * \sa Setting + */ +class SettingsObject : public QObject +{ + Q_OBJECT +public: + class Lock + { + public: + Lock(SettingsObjectPtr locked) + :m_locked(locked) + { + m_locked->suspendSave(); + } + ~Lock() + { + m_locked->resumeSave(); + } + private: + SettingsObjectPtr m_locked; + }; +public: + explicit SettingsObject(QObject *parent = 0); + virtual ~SettingsObject(); + /*! + * Registers an override setting for the given original setting in this settings object + * gate decides if the passthrough (true) or the original (false) is used for value + * + * This will fail if there is already a setting with the same ID as + * the one that is being registered. + * \return A valid Setting shared pointer if successful. + */ + std::shared_ptr registerOverride(std::shared_ptr original, std::shared_ptr gate); + + /*! + * Registers a passthorugh setting for the given original setting in this settings object + * gate decides if the passthrough (true) or the original (false) is used for value + * + * This will fail if there is already a setting with the same ID as + * the one that is being registered. + * \return A valid Setting shared pointer if successful. + */ + std::shared_ptr registerPassthrough(std::shared_ptr original, std::shared_ptr gate); + + /*! + * Registers the given setting with this SettingsObject and connects the necessary signals. + * + * This will fail if there is already a setting with the same ID as + * the one that is being registered. + * \return A valid Setting shared pointer if successful. + */ + std::shared_ptr registerSetting(QStringList synonyms, + QVariant defVal = QVariant()); + + /*! + * Registers the given setting with this SettingsObject and connects the necessary signals. + * + * This will fail if there is already a setting with the same ID as + * the one that is being registered. + * \return A valid Setting shared pointer if successful. + */ + std::shared_ptr registerSetting(QString id, QVariant defVal = QVariant()) + { + return registerSetting(QStringList(id), defVal); + } + + /*! + * \brief Gets the setting with the given ID. + * \param id The ID of the setting to get. + * \return A pointer to the setting with the given ID. + * Returns null if there is no setting with the given ID. + * \sa operator []() + */ + std::shared_ptr getSetting(const QString &id) const; + + /*! + * \brief Gets the value of the setting with the given ID. + * \param id The ID of the setting to get. + * \return The setting's value as a QVariant. + * If no setting with the given ID exists, returns an invalid QVariant. + */ + QVariant get(const QString &id) const; + + /*! + * \brief Sets the value of the setting with the given ID. + * If no setting with the given ID exists, returns false + * \param id The ID of the setting to change. + * \param value The new value of the setting. + * \return True if successful, false if it failed. + */ + bool set(const QString &id, QVariant value); + + /*! + * \brief Reverts the setting with the given ID to default. + * \param id The ID of the setting to reset. + */ + void reset(const QString &id) const; + + /*! + * \brief Checks if this SettingsObject contains a setting with the given ID. + * \param id The ID to check for. + * \return True if the SettingsObject has a setting with the given ID. + */ + bool contains(const QString &id); + + /*! + * \brief Reloads the settings and emit signals for changed settings + * \return True if reloading was successful + */ + virtual bool reload(); + + virtual void suspendSave() = 0; + virtual void resumeSave() = 0; +signals: + /*! + * \brief Signal emitted when one of this SettingsObject object's settings changes. + * This is usually just connected directly to each Setting object's + * SettingChanged() signals. + * \param setting A reference to the Setting object that changed. + * \param value The Setting object's new value. + */ + void SettingChanged(const Setting &setting, QVariant value); + + /*! + * \brief Signal emitted when one of this SettingsObject object's settings resets. + * This is usually just connected directly to each Setting object's + * settingReset() signals. + * \param setting A reference to the Setting object that changed. + */ + void settingReset(const Setting &setting); + +protected +slots: + /*! + * \brief Changes a setting. + * This slot is usually connected to each Setting object's + * SettingChanged() signal. The signal is emitted, causing this slot + * to update the setting's value in the config file. + * \param setting A reference to the Setting object that changed. + * \param value The setting's new value. + */ + virtual void changeSetting(const Setting &setting, QVariant value) = 0; + + /*! + * \brief Resets a setting. + * This slot is usually connected to each Setting object's + * settingReset() signal. The signal is emitted, causing this slot + * to update the setting's value in the config file. + * \param setting A reference to the Setting object that changed. + */ + virtual void resetSetting(const Setting &setting) = 0; + +protected: + /*! + * \brief Connects the necessary signals to the given Setting. + * \param setting The setting to connect. + */ + void connectSignals(const Setting &setting); + + /*! + * \brief Function used by Setting objects to get their values from the SettingsObject. + * \param setting The + * \return + */ + virtual QVariant retrieveValue(const Setting &setting) = 0; + + friend class Setting; + +private: + QMap> m_settings; +protected: + bool m_suspendSave = false; + bool m_doSave = false; +}; diff --git a/ultimmc/launcher/tasks/SequentialTask.cpp b/ultimmc/launcher/tasks/SequentialTask.cpp new file mode 100644 index 0000000..a66b9d7 --- /dev/null +++ b/ultimmc/launcher/tasks/SequentialTask.cpp @@ -0,0 +1,55 @@ +#include "SequentialTask.h" + +SequentialTask::SequentialTask(QObject *parent) : Task(parent), m_currentIndex(-1) +{ +} + +void SequentialTask::addTask(Task::Ptr task) +{ + m_queue.append(task); +} + +void SequentialTask::executeTask() +{ + m_currentIndex = -1; + startNext(); +} + +void SequentialTask::startNext() +{ + if (m_currentIndex != -1) + { + Task::Ptr previous = m_queue[m_currentIndex]; + disconnect(previous.get(), 0, this, 0); + } + m_currentIndex++; + if (m_queue.isEmpty() || m_currentIndex >= m_queue.size()) + { + emitSucceeded(); + return; + } + Task::Ptr next = m_queue[m_currentIndex]; + connect(next.get(), SIGNAL(failed(QString)), this, SLOT(subTaskFailed(QString))); + connect(next.get(), SIGNAL(status(QString)), this, SLOT(subTaskStatus(QString))); + connect(next.get(), SIGNAL(progress(qint64, qint64)), this, SLOT(subTaskProgress(qint64, qint64))); + connect(next.get(), SIGNAL(succeeded()), this, SLOT(startNext())); + next->start(); +} + +void SequentialTask::subTaskFailed(const QString &msg) +{ + emitFailed(msg); +} +void SequentialTask::subTaskStatus(const QString &msg) +{ + setStatus(msg); +} +void SequentialTask::subTaskProgress(qint64 current, qint64 total) +{ + if(total == 0) + { + setProgress(0, 100); + return; + } + setProgress(current, total); +} diff --git a/ultimmc/launcher/tasks/SequentialTask.h b/ultimmc/launcher/tasks/SequentialTask.h new file mode 100644 index 0000000..027744f --- /dev/null +++ b/ultimmc/launcher/tasks/SequentialTask.h @@ -0,0 +1,30 @@ +#pragma once + +#include "Task.h" +#include "QObjectPtr.h" + +#include + +class SequentialTask : public Task +{ + Q_OBJECT +public: + explicit SequentialTask(QObject *parent = 0); + virtual ~SequentialTask() {}; + + void addTask(Task::Ptr task); + +protected: + void executeTask(); + +private +slots: + void startNext(); + void subTaskFailed(const QString &msg); + void subTaskStatus(const QString &msg); + void subTaskProgress(qint64 current, qint64 total); + +private: + QQueue m_queue; + int m_currentIndex; +}; diff --git a/ultimmc/launcher/tasks/Task.cpp b/ultimmc/launcher/tasks/Task.cpp new file mode 100644 index 0000000..57307b4 --- /dev/null +++ b/ultimmc/launcher/tasks/Task.cpp @@ -0,0 +1,168 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Task.h" + +#include + +Task::Task(QObject *parent) : QObject(parent) +{ +} + +void Task::setStatus(const QString &new_status) +{ + if(m_status != new_status) + { + m_status = new_status; + emit status(m_status); + } +} + +void Task::setProgress(qint64 current, qint64 total) +{ + m_progress = current; + m_progressTotal = total; + emit progress(m_progress, m_progressTotal); +} + +void Task::start() +{ + switch(m_state) + { + case State::Inactive: + { + qDebug() << "Task" << describe() << "starting for the first time"; + break; + } + case State::AbortedByUser: + { + qDebug() << "Task" << describe() << "restarting for after being aborted by user"; + break; + } + case State::Failed: + { + qDebug() << "Task" << describe() << "restarting for after failing at first"; + break; + } + case State::Succeeded: + { + qDebug() << "Task" << describe() << "restarting for after succeeding at first"; + break; + } + case State::Running: + { + qWarning() << "The launcher tried to start task" << describe() << "while it was already running!"; + return; + } + } + // NOTE: only fall thorugh to here in end states + m_state = State::Running; + emit started(); + executeTask(); +} + +void Task::emitFailed(QString reason) +{ + // Don't fail twice. + if (!isRunning()) + { + qCritical() << "Task" << describe() << "failed while not running!!!!: " << reason; + return; + } + m_state = State::Failed; + m_failReason = reason; + qCritical() << "Task" << describe() << "failed: " << reason; + emit failed(reason); + emit finished(); +} + +void Task::emitAborted() +{ + // Don't abort twice. + if (!isRunning()) + { + qCritical() << "Task" << describe() << "aborted while not running!!!!"; + return; + } + m_state = State::AbortedByUser; + m_failReason = "Aborted."; + qDebug() << "Task" << describe() << "aborted."; + emit failed(m_failReason); + emit finished(); +} + +void Task::emitSucceeded() +{ + // Don't succeed twice. + if (!isRunning()) + { + qCritical() << "Task" << describe() << "succeeded while not running!!!!"; + return; + } + m_state = State::Succeeded; + qDebug() << "Task" << describe() << "succeeded"; + emit succeeded(); + emit finished(); +} + +QString Task::describe() +{ + QString outStr; + QTextStream out(&outStr); + out << metaObject()->className() << QChar('('); + auto name = objectName(); + if(name.isEmpty()) + { + out << QString("0x%1").arg((quintptr)this, 0, 16); + } + else + { + out << name; + } + out << QChar(')'); + out.flush(); + return outStr; +} + +bool Task::isRunning() const +{ + return m_state == State::Running; +} + +bool Task::isFinished() const +{ + return m_state != State::Running && m_state != State::Inactive; +} + +bool Task::wasSuccessful() const +{ + return m_state == State::Succeeded; +} + +QString Task::failReason() const +{ + return m_failReason; +} + +void Task::logWarning(const QString& line) +{ + qWarning() << line; + m_Warnings.append(line); +} + +QStringList Task::warnings() const +{ + return m_Warnings; +} diff --git a/ultimmc/launcher/tasks/Task.h b/ultimmc/launcher/tasks/Task.h new file mode 100644 index 0000000..9cf08db --- /dev/null +++ b/ultimmc/launcher/tasks/Task.h @@ -0,0 +1,110 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "QObjectPtr.h" + +class Task : public QObject +{ + Q_OBJECT +public: + using Ptr = shared_qobject_ptr; + + enum class State + { + Inactive, + Running, + Succeeded, + Failed, + AbortedByUser + }; + +public: + explicit Task(QObject *parent = 0); + virtual ~Task() {}; + + bool isRunning() const; + bool isFinished() const; + bool wasSuccessful() const; + + /*! + * Returns the string that was passed to emitFailed as the error message when the task failed. + * If the task hasn't failed, returns an empty string. + */ + QString failReason() const; + + virtual QStringList warnings() const; + + virtual bool canAbort() const { return false; } + + QString getStatus() + { + return m_status; + } + + qint64 getProgress() + { + return m_progress; + } + + qint64 getTotalProgress() + { + return m_progressTotal; + } + +protected: + void logWarning(const QString & line); + +private: + QString describe(); + +signals: + void started(); + void progress(qint64 current, qint64 total); + void finished(); + void succeeded(); + void failed(QString reason); + void status(QString status); + +public slots: + virtual void start(); + virtual bool abort() { return false; }; + +protected: + virtual void executeTask() = 0; + +protected slots: + virtual void emitSucceeded(); + virtual void emitAborted(); + virtual void emitFailed(QString reason); + +public slots: + void setStatus(const QString &status); + void setProgress(qint64 current, qint64 total); + +private: + State m_state = State::Inactive; + QStringList m_Warnings; + QString m_failReason = ""; + QString m_status; + int m_progress = 0; + int m_progressTotal = 100; +}; + diff --git a/ultimmc/launcher/testdata/FileSystem-test_createShortcut-unix b/ultimmc/launcher/testdata/FileSystem-test_createShortcut-unix new file mode 100755 index 0000000..1ce3a2b --- /dev/null +++ b/ultimmc/launcher/testdata/FileSystem-test_createShortcut-unix @@ -0,0 +1,6 @@ +[Desktop Entry] +Type=Application +TryExec=asdfDest +Exec=asdfDest 'arg1' 'arg2' +Name=asdf +Icon= diff --git a/ultimmc/launcher/testdata/test_folder/assets/minecraft/textures/blah.txt b/ultimmc/launcher/testdata/test_folder/assets/minecraft/textures/blah.txt new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/ultimmc/launcher/testdata/test_folder/assets/minecraft/textures/blah.txt @@ -0,0 +1 @@ + diff --git a/ultimmc/launcher/testdata/test_folder/pack.mcmeta b/ultimmc/launcher/testdata/test_folder/pack.mcmeta new file mode 100644 index 0000000..67ee043 --- /dev/null +++ b/ultimmc/launcher/testdata/test_folder/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "pack_format": 1, + "description": "Some resource pack maybe" + } +} diff --git a/ultimmc/launcher/testdata/test_folder/pack.nfo b/ultimmc/launcher/testdata/test_folder/pack.nfo new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/ultimmc/launcher/testdata/test_folder/pack.nfo @@ -0,0 +1 @@ + diff --git a/ultimmc/launcher/tools/BaseExternalTool.cpp b/ultimmc/launcher/tools/BaseExternalTool.cpp new file mode 100644 index 0000000..38d8178 --- /dev/null +++ b/ultimmc/launcher/tools/BaseExternalTool.cpp @@ -0,0 +1,41 @@ +#include "BaseExternalTool.h" + +#include +#include + +#ifdef Q_OS_WIN +#include +#endif + +#include "BaseInstance.h" + +BaseExternalTool::BaseExternalTool(SettingsObjectPtr settings, InstancePtr instance, QObject *parent) + : QObject(parent), m_instance(instance), globalSettings(settings) +{ +} + +BaseExternalTool::~BaseExternalTool() +{ +} + +BaseDetachedTool::BaseDetachedTool(SettingsObjectPtr settings, InstancePtr instance, QObject *parent) + : BaseExternalTool(settings, instance, parent) +{ + +} + +void BaseDetachedTool::run() +{ + runImpl(); +} + + +BaseExternalToolFactory::~BaseExternalToolFactory() +{ +} + +BaseDetachedTool *BaseDetachedToolFactory::createDetachedTool(InstancePtr instance, + QObject *parent) +{ + return qobject_cast(createTool(instance, parent)); +} diff --git a/ultimmc/launcher/tools/BaseExternalTool.h b/ultimmc/launcher/tools/BaseExternalTool.h new file mode 100644 index 0000000..1ebed6a --- /dev/null +++ b/ultimmc/launcher/tools/BaseExternalTool.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +class BaseInstance; +class SettingsObject; +class QProcess; + +class BaseExternalTool : public QObject +{ + Q_OBJECT +public: + explicit BaseExternalTool(SettingsObjectPtr settings, InstancePtr instance, QObject *parent = 0); + virtual ~BaseExternalTool(); + +protected: + InstancePtr m_instance; + SettingsObjectPtr globalSettings; +}; + +class BaseDetachedTool : public BaseExternalTool +{ + Q_OBJECT +public: + explicit BaseDetachedTool(SettingsObjectPtr settings, InstancePtr instance, QObject *parent = 0); + +public +slots: + void run(); + +protected: + virtual void runImpl() = 0; +}; + +class BaseExternalToolFactory +{ +public: + virtual ~BaseExternalToolFactory(); + + virtual QString name() const = 0; + + virtual void registerSettings(SettingsObjectPtr settings) = 0; + + virtual BaseExternalTool *createTool(InstancePtr instance, QObject *parent = 0) = 0; + + virtual bool check(QString *error) = 0; + virtual bool check(const QString &path, QString *error) = 0; + +protected: + SettingsObjectPtr globalSettings; +}; + +class BaseDetachedToolFactory : public BaseExternalToolFactory +{ +public: + virtual BaseDetachedTool *createDetachedTool(InstancePtr instance, QObject *parent = 0); +}; diff --git a/ultimmc/launcher/tools/BaseProfiler.cpp b/ultimmc/launcher/tools/BaseProfiler.cpp new file mode 100644 index 0000000..300d1a7 --- /dev/null +++ b/ultimmc/launcher/tools/BaseProfiler.cpp @@ -0,0 +1,36 @@ +#include "BaseProfiler.h" +#include "QObjectPtr.h" + +#include + +BaseProfiler::BaseProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject *parent) + : BaseExternalTool(settings, instance, parent) +{ +} + +void BaseProfiler::beginProfiling(shared_qobject_ptr process) +{ + beginProfilingImpl(process); +} + +void BaseProfiler::abortProfiling() +{ + abortProfilingImpl(); +} + +void BaseProfiler::abortProfilingImpl() +{ + if (!m_profilerProcess) + { + return; + } + m_profilerProcess->terminate(); + m_profilerProcess->deleteLater(); + m_profilerProcess = 0; + emit abortLaunch(tr("Profiler aborted")); +} + +BaseProfiler *BaseProfilerFactory::createProfiler(InstancePtr instance, QObject *parent) +{ + return qobject_cast(createTool(instance, parent)); +} diff --git a/ultimmc/launcher/tools/BaseProfiler.h b/ultimmc/launcher/tools/BaseProfiler.h new file mode 100644 index 0000000..1c934aa --- /dev/null +++ b/ultimmc/launcher/tools/BaseProfiler.h @@ -0,0 +1,37 @@ +#pragma once + +#include "BaseExternalTool.h" +#include "QObjectPtr.h" + +class BaseInstance; +class SettingsObject; +class LaunchTask; +class QProcess; + +class BaseProfiler : public BaseExternalTool +{ + Q_OBJECT +public: + explicit BaseProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject *parent = 0); + +public +slots: + void beginProfiling(shared_qobject_ptr process); + void abortProfiling(); + +protected: + QProcess *m_profilerProcess; + + virtual void beginProfilingImpl(shared_qobject_ptr process) = 0; + virtual void abortProfilingImpl(); + +signals: + void readyToLaunch(const QString &message); + void abortLaunch(const QString &message); +}; + +class BaseProfilerFactory : public BaseExternalToolFactory +{ +public: + virtual BaseProfiler *createProfiler(InstancePtr instance, QObject *parent = 0); +}; diff --git a/ultimmc/launcher/tools/JProfiler.cpp b/ultimmc/launcher/tools/JProfiler.cpp new file mode 100644 index 0000000..1dc0d10 --- /dev/null +++ b/ultimmc/launcher/tools/JProfiler.cpp @@ -0,0 +1,116 @@ +#include "JProfiler.h" + +#include + +#include "settings/SettingsObject.h" +#include "launch/LaunchTask.h" +#include "BaseInstance.h" + +class JProfiler : public BaseProfiler +{ + Q_OBJECT +public: + JProfiler(SettingsObjectPtr settings, InstancePtr instance, QObject *parent = 0); + +private slots: + void profilerStarted(); + void profilerFinished(int exit, QProcess::ExitStatus status); + +protected: + void beginProfilingImpl(shared_qobject_ptr process); + +private: + int listeningPort = 0; +}; + +JProfiler::JProfiler(SettingsObjectPtr settings, InstancePtr instance, + QObject *parent) + : BaseProfiler(settings, instance, parent) +{ +} + +void JProfiler::profilerStarted() +{ + emit readyToLaunch(tr("Listening on port: %1").arg(listeningPort)); +} + +void JProfiler::profilerFinished(int exit, QProcess::ExitStatus status) +{ + if (status == QProcess::CrashExit) + { + emit abortLaunch(tr("Profiler aborted")); + } + if (m_profilerProcess) + { + m_profilerProcess->deleteLater(); + m_profilerProcess = 0; + } +} + +void JProfiler::beginProfilingImpl(shared_qobject_ptr process) +{ + listeningPort = globalSettings->get("JProfilerPort").toInt(); + QProcess *profiler = new QProcess(this); + QStringList profilerArgs = + { + "-d", QString::number(process->pid()), + "--gui", + "-p", QString::number(listeningPort) + }; + auto basePath = globalSettings->get("JProfilerPath").toString(); + +#ifdef Q_OS_WIN + QString profilerProgram = QDir(basePath).absoluteFilePath("bin/jpenable.exe"); +#else + QString profilerProgram = QDir(basePath).absoluteFilePath("bin/jpenable"); +#endif + + profiler->setArguments(profilerArgs); + profiler->setProgram(profilerProgram); + + connect(profiler, SIGNAL(started()), SLOT(profilerStarted())); + connect(profiler, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(profilerFinished(int,QProcess::ExitStatus))); + + m_profilerProcess = profiler; + profiler->start(); +} + +void JProfilerFactory::registerSettings(SettingsObjectPtr settings) +{ + settings->registerSetting("JProfilerPath"); + settings->registerSetting("JProfilerPort", 42042); + globalSettings = settings; +} + +BaseExternalTool *JProfilerFactory::createTool(InstancePtr instance, QObject *parent) +{ + return new JProfiler(globalSettings, instance, parent); +} + +bool JProfilerFactory::check(QString *error) +{ + return check(globalSettings->get("JProfilerPath").toString(), error); +} + +bool JProfilerFactory::check(const QString &path, QString *error) +{ + if (path.isEmpty()) + { + *error = QObject::tr("Empty path"); + return false; + } + QDir dir(path); + if (!dir.exists()) + { + *error = QObject::tr("Path does not exist"); + return false; + } + if (!dir.exists("bin") || !(dir.exists("bin/jprofiler") || dir.exists("bin/jprofiler.exe")) || !dir.exists("bin/agent.jar")) + { + *error = QObject::tr("Invalid JProfiler install"); + return false; + } + return true; +} + +#include "JProfiler.moc" diff --git a/ultimmc/launcher/tools/JProfiler.h b/ultimmc/launcher/tools/JProfiler.h new file mode 100644 index 0000000..0e9a3a8 --- /dev/null +++ b/ultimmc/launcher/tools/JProfiler.h @@ -0,0 +1,13 @@ +#pragma once + +#include "BaseProfiler.h" + +class JProfilerFactory : public BaseProfilerFactory +{ +public: + QString name() const override { return "JProfiler"; } + void registerSettings(SettingsObjectPtr settings) override; + BaseExternalTool *createTool(InstancePtr instance, QObject *parent = 0) override; + bool check(QString *error) override; + bool check(const QString &path, QString *error) override; +}; diff --git a/ultimmc/launcher/tools/JVisualVM.cpp b/ultimmc/launcher/tools/JVisualVM.cpp new file mode 100644 index 0000000..b1acc3c --- /dev/null +++ b/ultimmc/launcher/tools/JVisualVM.cpp @@ -0,0 +1,104 @@ +#include "JVisualVM.h" + +#include +#include + +#include "settings/SettingsObject.h" +#include "launch/LaunchTask.h" +#include "BaseInstance.h" + +class JVisualVM : public BaseProfiler +{ + Q_OBJECT +public: + JVisualVM(SettingsObjectPtr settings, InstancePtr instance, QObject *parent = 0); + +private slots: + void profilerStarted(); + void profilerFinished(int exit, QProcess::ExitStatus status); + +protected: + void beginProfilingImpl(shared_qobject_ptr process); +}; + + +JVisualVM::JVisualVM(SettingsObjectPtr settings, InstancePtr instance, QObject *parent) + : BaseProfiler(settings, instance, parent) +{ +} + +void JVisualVM::profilerStarted() +{ + emit readyToLaunch(tr("JVisualVM started")); +} + +void JVisualVM::profilerFinished(int exit, QProcess::ExitStatus status) +{ + if (status == QProcess::CrashExit) + { + emit abortLaunch(tr("Profiler aborted")); + } + if (m_profilerProcess) + { + m_profilerProcess->deleteLater(); + m_profilerProcess = 0; + } +} + +void JVisualVM::beginProfilingImpl(shared_qobject_ptr process) +{ + QProcess *profiler = new QProcess(this); + QStringList profilerArgs = + { + "--openpid", QString::number(process->pid()) + }; + auto programPath = globalSettings->get("JVisualVMPath").toString(); + + profiler->setArguments(profilerArgs); + profiler->setProgram(programPath); + + connect(profiler, SIGNAL(started()), SLOT(profilerStarted())); + connect(profiler, SIGNAL(finished(int, QProcess::ExitStatus)), SLOT(profilerFinished(int,QProcess::ExitStatus))); + + profiler->start(); + m_profilerProcess = profiler; +} + +void JVisualVMFactory::registerSettings(SettingsObjectPtr settings) +{ + QString defaultValue = QStandardPaths::findExecutable("jvisualvm"); + if (defaultValue.isNull()) + { + defaultValue = QStandardPaths::findExecutable("visualvm"); + } + settings->registerSetting("JVisualVMPath", defaultValue); + globalSettings = settings; +} + +BaseExternalTool *JVisualVMFactory::createTool(InstancePtr instance, QObject *parent) +{ + return new JVisualVM(globalSettings, instance, parent); +} + +bool JVisualVMFactory::check(QString *error) +{ + return check(globalSettings->get("JVisualVMPath").toString(), error); +} + +bool JVisualVMFactory::check(const QString &path, QString *error) +{ + if (path.isEmpty()) + { + *error = QObject::tr("Empty path"); + return false; + } + QFileInfo finfo(path); + if (!finfo.isExecutable() || !finfo.fileName().contains("visualvm")) + { + *error = QObject::tr("Invalid path to JVisualVM"); + return false; + } + return true; +} + +#include "JVisualVM.moc" diff --git a/ultimmc/launcher/tools/JVisualVM.h b/ultimmc/launcher/tools/JVisualVM.h new file mode 100644 index 0000000..ebdea9f --- /dev/null +++ b/ultimmc/launcher/tools/JVisualVM.h @@ -0,0 +1,13 @@ +#pragma once + +#include "BaseProfiler.h" + +class JVisualVMFactory : public BaseProfilerFactory +{ +public: + QString name() const override { return "JVisualVM"; } + void registerSettings(SettingsObjectPtr settings) override; + BaseExternalTool *createTool(InstancePtr instance, QObject *parent = 0) override; + bool check(QString *error) override; + bool check(const QString &path, QString *error) override; +}; diff --git a/ultimmc/launcher/tools/MCEditTool.cpp b/ultimmc/launcher/tools/MCEditTool.cpp new file mode 100644 index 0000000..21e1a3b --- /dev/null +++ b/ultimmc/launcher/tools/MCEditTool.cpp @@ -0,0 +1,77 @@ +#include "MCEditTool.h" + +#include +#include +#include + +#include "settings/SettingsObject.h" +#include "BaseInstance.h" +#include "minecraft/MinecraftInstance.h" + +MCEditTool::MCEditTool(SettingsObjectPtr settings) +{ + settings->registerSetting("MCEditPath"); + m_settings = settings; +} + +void MCEditTool::setPath(QString& path) +{ + m_settings->set("MCEditPath", path); +} + +QString MCEditTool::path() const +{ + return m_settings->get("MCEditPath").toString(); +} + +bool MCEditTool::check(const QString& toolPath, QString& error) +{ + if (toolPath.isEmpty()) + { + error = QObject::tr("Path is empty"); + return false; + } + const QDir dir(toolPath); + if (!dir.exists()) + { + error = QObject::tr("Path does not exist"); + return false; + } + if (!dir.exists("mcedit.sh") && !dir.exists("mcedit.py") && !dir.exists("mcedit.exe") && !dir.exists("Contents") && !dir.exists("mcedit2.exe")) + { + error = QObject::tr("Path does not seem to be a MCEdit path"); + return false; + } + return true; +} + +QString MCEditTool::getProgramPath() +{ +#ifdef Q_OS_OSX + return path(); +#else + const QString mceditPath = path(); + QDir mceditDir(mceditPath); +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + if (mceditDir.exists("mcedit.sh")) + { + return mceditDir.absoluteFilePath("mcedit.sh"); + } + else if (mceditDir.exists("mcedit.py")) + { + return mceditDir.absoluteFilePath("mcedit.py"); + } + return QString(); +#elif defined(Q_OS_WIN32) + if (mceditDir.exists("mcedit.exe")) + { + return mceditDir.absoluteFilePath("mcedit.exe"); + } + else if (mceditDir.exists("mcedit2.exe")) + { + return mceditDir.absoluteFilePath("mcedit2.exe"); + } + return QString(); +#endif +#endif +} diff --git a/ultimmc/launcher/tools/MCEditTool.h b/ultimmc/launcher/tools/MCEditTool.h new file mode 100644 index 0000000..733dff8 --- /dev/null +++ b/ultimmc/launcher/tools/MCEditTool.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include "settings/SettingsObject.h" + +class MCEditTool +{ +public: + MCEditTool(SettingsObjectPtr settings); + void setPath(QString & path); + QString path() const; + bool check(const QString &toolPath, QString &error); + QString getProgramPath(); +private: + SettingsObjectPtr m_settings; +}; diff --git a/ultimmc/launcher/translations/POTranslator.cpp b/ultimmc/launcher/translations/POTranslator.cpp new file mode 100644 index 0000000..1ffcb9a --- /dev/null +++ b/ultimmc/launcher/translations/POTranslator.cpp @@ -0,0 +1,373 @@ +#include "POTranslator.h" + +#include +#include "FileSystem.h" + +struct POEntry +{ + QString text; + bool fuzzy; +}; + +struct POTranslatorPrivate +{ + QString filename; + QHash mapping; + QHash mapping_disambiguatrion; + bool loaded = false; + + void reload(); +}; + +class ParserArray : public QByteArray +{ +public: + ParserArray(const QByteArray &in) : QByteArray(in) + { + } + bool chomp(const char * data, int length) + { + if(startsWith(data)) + { + remove(0, length); + return true; + } + return false; + } + bool chompString(QByteArray & appendHere) + { + QByteArray msg; + bool escape = false; + if(size() < 2) + { + qDebug() << "String fragment is too short"; + return false; + } + if(!startsWith('"')) + { + qDebug() << "String fragment does not start with \""; + return false; + } + if(!endsWith('"')) + { + qDebug() << "String fragment does not end with \", instead, there is" << at(size() - 1); + return false; + } + for(int i = 1; i < size() - 1; i++) + { + char c = operator[](i); + if(escape) + { + switch(c) + { + case 'r': + msg += '\r'; + break; + case 'n': + msg += '\n'; + break; + case 't': + msg += '\t'; + break; + case 'v': + msg += '\v'; + break; + case 'a': + msg += '\a'; + break; + case 'b': + msg += '\b'; + break; + case 'f': + msg += '\f'; + break; + case '"': + msg += '"'; + break; + case '\\': + msg.append('\\'); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + { + int octal_start = i; + while ((c = operator[](i)) >= '0' && c <= '7') + { + i++; + if (i == length() - 1) + { + qDebug() << "Something went bad while parsing an octal escape string..."; + return false; + } + } + msg += mid(octal_start, i - octal_start).toUInt(0, 8); + break; + } + case 'x': + { + // chomp the 'x' + i++; + int hex_start = i; + while (isxdigit(operator[](i))) + { + i++; + if (i == length() - 1) + { + qDebug() << "Something went bad while parsing a hex escape string..."; + return false; + } + } + msg += mid(hex_start, i - hex_start).toUInt(0, 16); + break; + } + default: + { + qDebug() << "Invalid escape sequence character:" << c; + return false; + } + } + escape = false; + } + else if(c == '\\') + { + escape = true; + } + else + { + msg += c; + } + } + if(escape) + { + qDebug() << "Unterminated escape sequence..."; + return false; + } + appendHere += msg; + return true; + } +}; + +void POTranslatorPrivate::reload() +{ + QFile file(filename); + if(!file.open(QFile::OpenMode::enum_type::ReadOnly | QFile::OpenMode::enum_type::Text)) + { + qDebug() << "Failed to open PO file:" << filename; + return; + } + + QByteArray context; + QByteArray disambiguation; + QByteArray id; + QByteArray str; + bool fuzzy = false; + bool nextFuzzy = false; + + enum class Mode + { + First, + MessageContext, + MessageId, + MessageString + } mode = Mode::First; + + int lineNumber = 0; + QHash newMapping; + QHash newMapping_disambiguation; + auto endEntry = [&]() { + auto strStr = QString::fromUtf8(str); + // NOTE: PO header has empty id. We skip it. + if(!id.isEmpty()) + { + auto normalKey = context + "|" + id; + newMapping.insert(normalKey, {strStr, fuzzy}); + if(!disambiguation.isEmpty()) + { + auto disambiguationKey = context + "|" + id + "@" + disambiguation; + newMapping_disambiguation.insert(disambiguationKey, {strStr, fuzzy}); + } + } + context.clear(); + disambiguation.clear(); + id.clear(); + str.clear(); + fuzzy = nextFuzzy; + nextFuzzy = false; + }; + while (!file.atEnd()) + { + ParserArray line = file.readLine(); + if(line.endsWith('\n')) + { + line.resize(line.size() - 1); + } + if(line.endsWith('\r')) + { + line.resize(line.size() - 1); + } + + if(!line.size()) + { + // NIL + } + else if(line[0] == '#') + { + if(line.contains(", fuzzy")) + { + nextFuzzy = true; + } + } + else if(line.startsWith('"')) + { + QByteArray temp; + QByteArray *out = &temp; + + switch(mode) + { + case Mode::First: + qDebug() << "Unexpected escaped string during initial state... line:" << lineNumber; + return; + case Mode::MessageString: + out = &str; + break; + case Mode::MessageContext: + out = &context; + break; + case Mode::MessageId: + out = &id; + break; + } + if(!line.chompString(*out)) + { + qDebug() << "Badly formatted string on line:" << lineNumber; + return; + } + } + else if(line.chomp("msgctxt ", 8)) + { + switch(mode) + { + case Mode::First: + break; + case Mode::MessageString: + endEntry(); + break; + case Mode::MessageContext: + case Mode::MessageId: + qDebug() << "Unexpected msgctxt line:" << lineNumber; + return; + } + if(line.chompString(context)) + { + auto parts = context.split('|'); + context = parts[0]; + if(parts.size() > 1 && !parts[1].isEmpty()) + { + disambiguation = parts[1]; + } + mode = Mode::MessageContext; + } + } + else if (line.chomp("msgid ", 6)) + { + switch(mode) + { + case Mode::MessageContext: + case Mode::First: + break; + case Mode::MessageString: + endEntry(); + break; + case Mode::MessageId: + qDebug() << "Unexpected msgid line:" << lineNumber; + return; + } + if(line.chompString(id)) + { + mode = Mode::MessageId; + } + } + else if (line.chomp("msgstr ", 7)) + { + switch(mode) + { + case Mode::First: + case Mode::MessageString: + case Mode::MessageContext: + qDebug() << "Unexpected msgstr line:" << lineNumber; + return; + case Mode::MessageId: + break; + } + if(line.chompString(str)) + { + mode = Mode::MessageString; + } + } + else + { + qDebug() << "I did not understand line: " << lineNumber << ":" << QString::fromUtf8(line); + } + lineNumber++; + } + endEntry(); + mapping = std::move(newMapping); + mapping_disambiguatrion = std::move(newMapping_disambiguation); + loaded = true; +} + +POTranslator::POTranslator(const QString& filename, QObject* parent) : QTranslator(parent) +{ + d = new POTranslatorPrivate; + d->filename = filename; + d->reload(); +} + +QString POTranslator::translate(const char* context, const char* sourceText, const char* disambiguation, int n) const +{ + if(disambiguation) + { + auto disambiguationKey = QByteArray(context) + "|" + QByteArray(sourceText) + "@" + QByteArray(disambiguation); + auto iter = d->mapping_disambiguatrion.find(disambiguationKey); + if(iter != d->mapping_disambiguatrion.end()) + { + auto & entry = *iter; + if(entry.text.isEmpty()) + { + qDebug() << "Translation entry has no content:" << disambiguationKey; + } + if(entry.fuzzy) + { + qDebug() << "Translation entry is fuzzy:" << disambiguationKey << "->" << entry.text; + } + return entry.text; + } + } + auto key = QByteArray(context) + "|" + QByteArray(sourceText); + auto iter = d->mapping.find(key); + if(iter != d->mapping.end()) + { + auto & entry = *iter; + if(entry.text.isEmpty()) + { + qDebug() << "Translation entry has no content:" << key; + } + if(entry.fuzzy) + { + qDebug() << "Translation entry is fuzzy:" << key << "->" << entry.text; + } + return entry.text; + } + return QString(); +} + +bool POTranslator::isEmpty() const +{ + return !d->loaded; +} diff --git a/ultimmc/launcher/translations/POTranslator.h b/ultimmc/launcher/translations/POTranslator.h new file mode 100644 index 0000000..6d51856 --- /dev/null +++ b/ultimmc/launcher/translations/POTranslator.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +struct POTranslatorPrivate; + +class POTranslator : public QTranslator +{ + Q_OBJECT +public: + explicit POTranslator(const QString& filename, QObject * parent = nullptr); + QString translate(const char * context, const char * sourceText, const char * disambiguation, int n) const override; + bool isEmpty() const override; +private: + POTranslatorPrivate * d; +}; diff --git a/ultimmc/launcher/translations/TranslationsModel.cpp b/ultimmc/launcher/translations/TranslationsModel.cpp new file mode 100644 index 0000000..3ce8939 --- /dev/null +++ b/ultimmc/launcher/translations/TranslationsModel.cpp @@ -0,0 +1,667 @@ +#include "TranslationsModel.h" + +#include +#include +#include +#include +#include +#include + +#include "FileSystem.h" +#include "net/NetJob.h" +#include "net/ChecksumValidator.h" +#include "BuildConfig.h" +#include "Json.h" + +#include "POTranslator.h" + +#include "Application.h" + +const static QLatin1Literal defaultLangCode("en_US"); + +enum class FileType +{ + NONE, + QM, + PO +}; + +struct Language +{ + Language() + { + updated = true; + } + Language(const QString & _key) + { + key = _key; + locale = QLocale(key); + updated = (key == defaultLangCode); + } + + QString languageName() const { + QString result; + if(key == "ja_KANJI") { + result = locale.nativeLanguageName() + u8" (漢字)"; + } + else if(key == "es_UY") { + result = u8"español de Latinoamérica"; + } + else { + result = locale.nativeLanguageName(); + } + return result; + } + + float percentTranslated() const + { + if (total == 0) + { + return 100.0f; + } + return 100.0f * float(translated) / float(total); + } + + void setTranslationStats(unsigned _translated, unsigned _untranslated, unsigned _fuzzy) + { + translated = _translated; + untranslated = _untranslated; + fuzzy = _fuzzy; + total = translated + untranslated + fuzzy; + } + + bool isOfSameNameAs(const Language& other) const + { + return key == other.key; + } + + bool isIdenticalTo(const Language& other) const + { + return + ( + key == other.key && + file_name == other.file_name && + file_size == other.file_size && + file_sha1 == other.file_sha1 && + translated == other.translated && + fuzzy == other.fuzzy && + total == other.fuzzy && + localFileType == other.localFileType + ); + } + + Language & apply(Language & other) + { + if(!isOfSameNameAs(other)) + { + return *this; + } + file_name = other.file_name; + file_size = other.file_size; + file_sha1 = other.file_sha1; + translated = other.translated; + fuzzy = other.fuzzy; + total = other.total; + localFileType = other.localFileType; + return *this; + } + + QString key; + QLocale locale; + bool updated; + + QString file_name = QString(); + std::size_t file_size = 0; + QString file_sha1 = QString(); + + unsigned translated = 0; + unsigned untranslated = 0; + unsigned fuzzy = 0; + unsigned total = 0; + + FileType localFileType = FileType::NONE; +}; + + + +struct TranslationsModel::Private +{ + QDir m_dir; + + // initial state is just english + QVector m_languages = {Language (defaultLangCode)}; + + QString m_selectedLanguage = defaultLangCode; + std::unique_ptr m_qt_translator; + std::unique_ptr m_app_translator; + + Net::Download::Ptr m_index_task; + QString m_downloadingTranslation; + NetJob::Ptr m_dl_job; + NetJob::Ptr m_index_job; + QString m_nextDownload; + + std::unique_ptr m_po_translator; + QFileSystemWatcher *watcher; +}; + +TranslationsModel::TranslationsModel(QString path, QObject* parent): QAbstractListModel(parent) +{ + d.reset(new Private); + d->m_dir.setPath(path); + FS::ensureFolderPathExists(path); + reloadLocalFiles(); + + d->watcher = new QFileSystemWatcher(this); + connect(d->watcher, &QFileSystemWatcher::directoryChanged, this, &TranslationsModel::translationDirChanged); + d->watcher->addPath(d->m_dir.canonicalPath()); +} + +TranslationsModel::~TranslationsModel() +{ +} + +void TranslationsModel::translationDirChanged(const QString& path) +{ + qDebug() << "Dir changed:" << path; + reloadLocalFiles(); + selectLanguage(selectedLanguage()); +} + +void TranslationsModel::indexReceived() +{ + qDebug() << "Got translations index!"; + d->m_index_job.reset(); + if(d->m_selectedLanguage != defaultLangCode) + { + downloadTranslation(d->m_selectedLanguage); + } +} + +namespace { +void readIndex(const QString & path, QMap& languages) +{ + QByteArray data; + try + { + data = FS::read(path); + } + catch (const Exception &e) + { + qCritical() << "Translations Download Failed: index file not readable"; + return; + } + + try + { + auto toplevel_doc = Json::requireDocument(data); + auto doc = Json::requireObject(toplevel_doc); + auto file_type = Json::requireString(doc, "file_type"); + if(file_type != "MMC-TRANSLATION-INDEX") + { + qCritical() << "Translations Download Failed: index file is of unknown file type" << file_type; + return; + } + auto version = Json::requireInteger(doc, "version"); + if(version > 2) + { + qCritical() << "Translations Download Failed: index file is of unknown format version" << file_type; + return; + } + auto langObjs = Json::requireObject(doc, "languages"); + for(auto iter = langObjs.begin(); iter != langObjs.end(); iter++) + { + Language lang(iter.key()); + + auto langObj = Json::requireValueObject(iter.value()); + lang.setTranslationStats( + Json::ensureInteger(langObj, "translated", 0), + Json::ensureInteger(langObj, "untranslated", 0), + Json::ensureInteger(langObj, "fuzzy", 0) + ); + lang.file_name = Json::requireString(langObj, "file"); + lang.file_sha1 = Json::requireString(langObj, "sha1"); + lang.file_size = Json::requireInteger(langObj, "size"); + + languages.insert(lang.key, lang); + } + } + catch (Json::JsonException & e) + { + qCritical() << "Translations Download Failed: index file could not be parsed as json"; + } +} +} + +void TranslationsModel::reloadLocalFiles() +{ + QMap languages = {{defaultLangCode, Language(defaultLangCode)}}; + + readIndex(d->m_dir.absoluteFilePath("index_v2.json"), languages); + auto entries = d->m_dir.entryInfoList({"mmc_*.qm", "*.po"}, QDir::Files | QDir::NoDotAndDotDot); + for(auto & entry: entries) + { + auto completeSuffix = entry.completeSuffix(); + QString langCode; + FileType fileType = FileType::NONE; + if(completeSuffix == "qm") + { + langCode = entry.baseName().remove(0,4); + fileType = FileType::QM; + } + else if(completeSuffix == "po") + { + langCode = entry.baseName(); + fileType = FileType::PO; + } + else + { + continue; + } + + auto langIter = languages.find(langCode); + if(langIter != languages.end()) + { + auto & language = *langIter; + if(int(fileType) > int(language.localFileType)) + { + language.localFileType = fileType; + } + } + else + { + if(fileType == FileType::PO) + { + Language localFound(langCode); + localFound.localFileType = FileType::PO; + languages.insert(langCode, localFound); + } + } + } + + // changed and removed languages + for(auto iter = d->m_languages.begin(); iter != d->m_languages.end();) + { + auto &language = *iter; + auto row = iter - d->m_languages.begin(); + + auto updatedLanguageIter = languages.find(language.key); + if(updatedLanguageIter != languages.end()) + { + if(language.isIdenticalTo(*updatedLanguageIter)) + { + languages.remove(language.key); + } + else + { + language.apply(*updatedLanguageIter); + emit dataChanged(index(row), index(row)); + languages.remove(language.key); + } + iter++; + } + else + { + beginRemoveRows(QModelIndex(), row, row); + iter = d->m_languages.erase(iter); + endRemoveRows(); + } + } + // added languages + if(languages.isEmpty()) + { + return; + } + beginInsertRows(QModelIndex(), 0, d->m_languages.size() + languages.size() - 1); + for(auto & language: languages) + { + d->m_languages.append(language); + } + std::sort(d->m_languages.begin(), d->m_languages.end(), [](const Language& a, const Language& b) { + return a.key.compare(b.key) < 0; + }); + endInsertRows(); +} + +namespace { +enum class Column +{ + Language, + Completeness +}; +} + + +QVariant TranslationsModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + auto column = static_cast(index.column()); + + if (row < 0 || row >= d->m_languages.size()) + return QVariant(); + + auto & lang = d->m_languages[row]; + switch (role) + { + case Qt::DisplayRole: + { + switch(column) + { + case Column::Language: + { + return lang.languageName(); + } + case Column::Completeness: + { + QString text; + text.sprintf("%3.1f %%", lang.percentTranslated()); + return text; + } + } + } + case Qt::ToolTipRole: + { + return tr("%1:\n%2 translated\n%3 fuzzy\n%4 total").arg(lang.key, QString::number(lang.translated), QString::number(lang.fuzzy), QString::number(lang.total)); + } + case Qt::UserRole: + return lang.key; + default: + return QVariant(); + } +} + +QVariant TranslationsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + auto column = static_cast(section); + if(role == Qt::DisplayRole) + { + switch(column) + { + case Column::Language: + { + return tr("Language"); + } + case Column::Completeness: + { + return tr("Completeness"); + } + } + } + else if(role == Qt::ToolTipRole) + { + switch(column) + { + case Column::Language: + { + return tr("The native language name."); + } + case Column::Completeness: + { + return tr("Completeness is the percentage of fully translated strings, not counting automatically guessed ones."); + } + } + } + return QAbstractListModel::headerData(section, orientation, role); +} + +int TranslationsModel::rowCount(const QModelIndex& parent) const +{ + return d->m_languages.size(); +} + +int TranslationsModel::columnCount(const QModelIndex& parent) const +{ + return 2; +} + +Language * TranslationsModel::findLanguage(const QString& key) +{ + auto found = std::find_if(d->m_languages.begin(), d->m_languages.end(), [&](Language & lang) + { + return lang.key == key; + }); + if(found == d->m_languages.end()) + { + return nullptr; + } + else + { + return found; + } +} + +bool TranslationsModel::selectLanguage(QString key) +{ + QString &langCode = key; + auto langPtr = findLanguage(key); + if(!langPtr) + { + qWarning() << "Selected invalid language" << key << ", defaulting to" << defaultLangCode; + langCode = defaultLangCode; + } + else + { + langCode = langPtr->key; + } + + // uninstall existing translators if there are any + if (d->m_app_translator) + { + QCoreApplication::removeTranslator(d->m_app_translator.get()); + d->m_app_translator.reset(); + } + if (d->m_qt_translator) + { + QCoreApplication::removeTranslator(d->m_qt_translator.get()); + d->m_qt_translator.reset(); + } + + /* + * FIXME: potential source of crashes: + * In a multithreaded application, the default locale should be set at application startup, before any non-GUI threads are created. + * This function is not reentrant. + */ + QLocale locale = QLocale(langCode); + QLocale::setDefault(locale); + + // if it's the default UI language, finish + if(langCode == defaultLangCode) + { + d->m_selectedLanguage = langCode; + return true; + } + + // otherwise install new translations + bool successful = false; + // FIXME: this is likely never present. FIX IT. + d->m_qt_translator.reset(new QTranslator()); + if (d->m_qt_translator->load("qt_" + langCode, QLibraryInfo::location(QLibraryInfo::TranslationsPath))) + { + qDebug() << "Loading Qt Language File for" << langCode.toLocal8Bit().constData() << "..."; + if (!QCoreApplication::installTranslator(d->m_qt_translator.get())) + { + qCritical() << "Loading Qt Language File failed."; + d->m_qt_translator.reset(); + } + else + { + successful = true; + } + } + else + { + d->m_qt_translator.reset(); + } + + if(langPtr->localFileType == FileType::PO) + { + qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "..."; + auto poTranslator = new POTranslator(FS::PathCombine(d->m_dir.path(), langCode + ".po")); + if(!poTranslator->isEmpty()) + { + if (!QCoreApplication::installTranslator(poTranslator)) + { + delete poTranslator; + qCritical() << "Installing Application Language File failed."; + } + else + { + d->m_app_translator.reset(poTranslator); + successful = true; + } + } + else + { + qCritical() << "Loading Application Language File failed."; + d->m_app_translator.reset(); + } + } + else if(langPtr->localFileType == FileType::QM) + { + d->m_app_translator.reset(new QTranslator()); + if (d->m_app_translator->load("mmc_" + langCode, d->m_dir.path())) + { + qDebug() << "Loading Application Language File for" << langCode.toLocal8Bit().constData() << "..."; + if (!QCoreApplication::installTranslator(d->m_app_translator.get())) + { + qCritical() << "Installing Application Language File failed."; + d->m_app_translator.reset(); + } + else + { + successful = true; + } + } + else + { + d->m_app_translator.reset(); + } + } + else + { + d->m_app_translator.reset(); + } + d->m_selectedLanguage = langCode; + return successful; +} + +QModelIndex TranslationsModel::selectedIndex() +{ + auto found = findLanguage(d->m_selectedLanguage); + if(found) + { + // QVector iterator freely converts to pointer to contained type + return index(found - d->m_languages.begin(), 0, QModelIndex()); + } + return QModelIndex(); +} + +QString TranslationsModel::selectedLanguage() +{ + return d->m_selectedLanguage; +} + +void TranslationsModel::downloadIndex() +{ + if(d->m_index_job || d->m_dl_job) + { + return; + } + qDebug() << "Downloading Translations Index..."; + d->m_index_job = new NetJob("Translations Index", APPLICATION->network()); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "index_v2.json"); + entry->setStale(true); + d->m_index_task = Net::Download::makeCached(QUrl("https://files.multimc.org/translations/index_v2.json"), entry); + d->m_index_job->addNetAction(d->m_index_task); + connect(d->m_index_job.get(), &NetJob::failed, this, &TranslationsModel::indexFailed); + connect(d->m_index_job.get(), &NetJob::succeeded, this, &TranslationsModel::indexReceived); + d->m_index_job->start(); +} + +void TranslationsModel::updateLanguage(QString key) +{ + if(key == defaultLangCode) + { + qWarning() << "Cannot update builtin language" << key; + return; + } + auto found = findLanguage(key); + if(!found) + { + qWarning() << "Cannot update invalid language" << key; + return; + } + if(!found->updated) + { + downloadTranslation(key); + } +} + +void TranslationsModel::downloadTranslation(QString key) +{ + if(d->m_dl_job) + { + d->m_nextDownload = key; + return; + } + auto lang = findLanguage(key); + if(!lang) + { + qWarning() << "Will not download an unknown translation" << key; + return; + } + + d->m_downloadingTranslation = key; + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("translations", "mmc_" + key + ".qm"); + entry->setStale(true); + + auto dl = Net::Download::makeCached(QUrl(BuildConfig.TRANSLATIONS_BASE_URL + lang->file_name), entry); + auto rawHash = QByteArray::fromHex(lang->file_sha1.toLatin1()); + dl->addValidator(new Net::ChecksumValidator(QCryptographicHash::Sha1, rawHash)); + dl->m_total_progress = lang->file_size; + + d->m_dl_job = new NetJob("Translation for " + key, APPLICATION->network()); + d->m_dl_job->addNetAction(dl); + + connect(d->m_dl_job.get(), &NetJob::succeeded, this, &TranslationsModel::dlGood); + connect(d->m_dl_job.get(), &NetJob::failed, this, &TranslationsModel::dlFailed); + + d->m_dl_job->start(); +} + +void TranslationsModel::downloadNext() +{ + if(!d->m_nextDownload.isEmpty()) + { + downloadTranslation(d->m_nextDownload); + d->m_nextDownload.clear(); + } +} + +void TranslationsModel::dlFailed(QString reason) +{ + qCritical() << "Translations Download Failed:" << reason; + d->m_dl_job.reset(); + downloadNext(); +} + +void TranslationsModel::dlGood() +{ + qDebug() << "Got translation:" << d->m_downloadingTranslation; + + if(d->m_downloadingTranslation == d->m_selectedLanguage) + { + selectLanguage(d->m_selectedLanguage); + } + d->m_dl_job.reset(); + downloadNext(); +} + +void TranslationsModel::indexFailed(QString reason) +{ + qCritical() << "Translations Index Download Failed:" << reason; + d->m_index_job.reset(); +} diff --git a/ultimmc/launcher/translations/TranslationsModel.h b/ultimmc/launcher/translations/TranslationsModel.h new file mode 100644 index 0000000..3abf84e --- /dev/null +++ b/ultimmc/launcher/translations/TranslationsModel.h @@ -0,0 +1,64 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +struct Language; + +class TranslationsModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit TranslationsModel(QString path, QObject *parent = 0); + virtual ~TranslationsModel(); + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex & parent) const override; + + bool selectLanguage(QString key); + void updateLanguage(QString key); + QModelIndex selectedIndex(); + QString selectedLanguage(); + + void downloadIndex(); + +private: + Language *findLanguage(const QString & key); + void reloadLocalFiles(); + void downloadTranslation(QString key); + void downloadNext(); + + // hide copy constructor + TranslationsModel(const TranslationsModel &) = delete; + // hide assign op + TranslationsModel &operator=(const TranslationsModel &) = delete; + +private slots: + void indexReceived(); + void indexFailed(QString reason); + void dlFailed(QString reason); + void dlGood(); + void translationDirChanged(const QString &path); + + +private: /* data */ + struct Private; + std::unique_ptr d; +}; diff --git a/ultimmc/launcher/ui/ColorCache.cpp b/ultimmc/launcher/ui/ColorCache.cpp new file mode 100644 index 0000000..ef268dd --- /dev/null +++ b/ultimmc/launcher/ui/ColorCache.cpp @@ -0,0 +1,35 @@ +#include "ColorCache.h" + + +/** + * Blend the color with the front color, adapting to the back color + */ +QColor ColorCache::blend(QColor color) +{ + if (Rainbow::luma(m_front) > Rainbow::luma(m_back)) + { + // for dark color schemes, produce a fitting color first + color = Rainbow::tint(m_front, color, 0.5); + } + // adapt contrast + return Rainbow::mix(m_front, color, m_bias); +} + +/** + * Blend the color with the back color + */ +QColor ColorCache::blendBackground(QColor color) +{ + // adapt contrast + return Rainbow::mix(m_back, color, m_bias); +} + +void ColorCache::recolorAll() +{ + auto iter = m_colors.begin(); + while(iter != m_colors.end()) + { + iter->front = blend(iter->original); + iter->back = blendBackground(iter->original); + } +} diff --git a/ultimmc/launcher/ui/ColorCache.h b/ultimmc/launcher/ui/ColorCache.h new file mode 100644 index 0000000..a840664 --- /dev/null +++ b/ultimmc/launcher/ui/ColorCache.h @@ -0,0 +1,119 @@ +#pragma once +#include +#include +#include +#include + +class ColorCache +{ +public: + ColorCache(QColor front, QColor back, qreal bias) + { + m_front = front; + m_back = back; + m_bias = bias; + }; + + void addColor(int key, QColor color) + { + m_colors[key] = {color, blend(color), blendBackground(color)}; + } + + void setForeground(QColor front) + { + if(m_front != front) + { + m_front = front; + recolorAll(); + } + } + + void setBackground(QColor back) + { + if(m_back != back) + { + m_back = back; + recolorAll(); + } + } + + QColor getFront(int key) + { + auto iter = m_colors.find(key); + if(iter == m_colors.end()) + { + return QColor(); + } + return (*iter).front; + } + + QColor getBack(int key) + { + auto iter = m_colors.find(key); + if(iter == m_colors.end()) + { + return QColor(); + } + return (*iter).back; + } + + /** + * Blend the color with the front color, adapting to the back color + */ + QColor blend(QColor color); + + /** + * Blend the color with the back color + */ + QColor blendBackground(QColor color); + +protected: + void recolorAll(); + +protected: + struct ColorEntry + { + QColor original; + QColor front; + QColor back; + }; + +protected: + qreal m_bias; + QColor m_front; + QColor m_back; + QMap m_colors; +}; + +class LogColorCache : public ColorCache +{ +public: + LogColorCache(QColor front, QColor back) + : ColorCache(front, back, 1.0) + { + addColor((int)MessageLevel::Launcher, QColor("purple")); + addColor((int)MessageLevel::Debug, QColor("green")); + addColor((int)MessageLevel::Warning, QColor("orange")); + addColor((int)MessageLevel::Error, QColor("red")); + addColor((int)MessageLevel::Fatal, QColor("red")); + addColor((int)MessageLevel::Message, front); + } + + QColor getFront(MessageLevel::Enum level) + { + if(!m_colors.contains((int) level)) + { + return ColorCache::getFront((int)MessageLevel::Message); + } + return ColorCache::getFront((int)level); + } + + QColor getBack(MessageLevel::Enum level) + { + if(level == MessageLevel::Fatal) + { + return QColor(Qt::black); + } + return QColor(Qt::transparent); + } +}; diff --git a/ultimmc/launcher/ui/GuiUtil.cpp b/ultimmc/launcher/ui/GuiUtil.cpp new file mode 100644 index 0000000..efb1a4d --- /dev/null +++ b/ultimmc/launcher/ui/GuiUtil.cpp @@ -0,0 +1,136 @@ +#include "GuiUtil.h" + +#include +#include +#include + +#include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "net/PasteUpload.h" + +#include "Application.h" +#include +#include +#include + +QString GuiUtil::uploadPaste(const QString &text, QWidget *parentWidget) +{ + ProgressDialog dialog(parentWidget); + auto APIKeySetting = APPLICATION->settings()->get("PasteEEAPIKey").toString(); + if(APIKeySetting == "multimc") + { + APIKeySetting = BuildConfig.PASTE_EE_KEY; + } + std::unique_ptr paste(new PasteUpload(parentWidget, text, APIKeySetting)); + + if (!paste->validateText()) + { + CustomMessageBox::selectable( + parentWidget, QObject::tr("Upload failed"), + QObject::tr("The log file is too big. You'll have to upload it manually."), + QMessageBox::Warning)->exec(); + return QString(); + } + + dialog.execWithTask(paste.get()); + if (!paste->wasSuccessful()) + { + CustomMessageBox::selectable( + parentWidget, + QObject::tr("Upload failed"), + paste->failReason(), + QMessageBox::Critical + )->exec(); + return QString(); + } + else + { + const QString link = paste->pasteLink(); + setClipboardText(link); + CustomMessageBox::selectable( + parentWidget, QObject::tr("Upload finished"), + QObject::tr("The link to the uploaded log has been placed in your clipboard.").arg(link), + QMessageBox::Information)->exec(); + return link; + } +} + +void GuiUtil::setClipboardText(const QString &text) +{ + QApplication::clipboard()->setText(text); +} + +static QStringList BrowseForFileInternal(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget, bool single) +{ + static QMap savedPaths; + + QFileDialog w(parentWidget, caption); + QSet locations; + auto f = [&](QStandardPaths::StandardLocation l) + { + QString location = QStandardPaths::writableLocation(l); + QFileInfo finfo(location); + if (!finfo.exists()) { + return; + } + locations.insert(location); + }; + f(QStandardPaths::DesktopLocation); + f(QStandardPaths::DocumentsLocation); + f(QStandardPaths::DownloadLocation); + f(QStandardPaths::HomeLocation); + QList urls; + for (auto location : locations) + { + urls.append(QUrl::fromLocalFile(location)); + } + urls.append(QUrl::fromLocalFile(defaultPath)); + + w.setFileMode(single ? QFileDialog::ExistingFile : QFileDialog::ExistingFiles); + w.setAcceptMode(QFileDialog::AcceptOpen); + w.setNameFilter(filter); + + QString pathToOpen; + if(savedPaths.contains(context)) + { + pathToOpen = savedPaths[context]; + } + else + { + pathToOpen = defaultPath; + } + if(!pathToOpen.isEmpty()) + { + QFileInfo finfo(pathToOpen); + if(finfo.exists() && finfo.isDir()) + { + w.setDirectory(finfo.absoluteFilePath()); + } + } + + w.setSidebarUrls(urls); + + if (w.exec()) + { + savedPaths[context] = w.directory().absolutePath(); + return w.selectedFiles(); + } + savedPaths[context] = w.directory().absolutePath(); + return {}; +} + +QString GuiUtil::BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget) +{ + auto resultList = BrowseForFileInternal(context, caption, filter, defaultPath, parentWidget, true); + if(resultList.size()) + { + return resultList[0]; + } + return QString(); +} + + +QStringList GuiUtil::BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget) +{ + return BrowseForFileInternal(context, caption, filter, defaultPath, parentWidget, false); +} diff --git a/ultimmc/launcher/ui/GuiUtil.h b/ultimmc/launcher/ui/GuiUtil.h new file mode 100644 index 0000000..5e10938 --- /dev/null +++ b/ultimmc/launcher/ui/GuiUtil.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace GuiUtil +{ +QString uploadPaste(const QString &text, QWidget *parentWidget); +void setClipboardText(const QString &text); +QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget); +QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget); +} diff --git a/ultimmc/launcher/ui/InstanceWindow.cpp b/ultimmc/launcher/ui/InstanceWindow.cpp new file mode 100644 index 0000000..ae765c3 --- /dev/null +++ b/ultimmc/launcher/ui/InstanceWindow.cpp @@ -0,0 +1,237 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "InstanceWindow.h" +#include "Application.h" + +#include +#include +#include +#include +#include +#include + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/widgets/PageContainer.h" + +#include "InstancePageProvider.h" + +#include "icons/IconList.h" + +InstanceWindow::InstanceWindow(InstancePtr instance, QWidget *parent) + : QMainWindow(parent), m_instance(instance) +{ + setAttribute(Qt::WA_DeleteOnClose); + + auto icon = APPLICATION->icons()->getIcon(m_instance->iconKey()); + QString windowTitle = tr("Console window for ") + m_instance->name(); + + // Set window properties + { + setWindowIcon(icon); + setWindowTitle(windowTitle); + } + + // Add page container + { + auto provider = std::make_shared(m_instance); + m_container = new PageContainer(provider.get(), "console", this); + m_container->setParentContainer(this); + setCentralWidget(m_container); + setContentsMargins(0, 0, 0, 0); + } + + // Add custom buttons to the page container layout. + { + auto horizontalLayout = new QHBoxLayout(); + horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); + horizontalLayout->setContentsMargins(6, -1, 6, -1); + + auto btnHelp = new QPushButton(); + btnHelp->setText(tr("Help")); + horizontalLayout->addWidget(btnHelp); + connect(btnHelp, SIGNAL(clicked(bool)), m_container, SLOT(help())); + + auto spacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); + horizontalLayout->addSpacerItem(spacer); + + m_killButton = new QPushButton(); + horizontalLayout->addWidget(m_killButton); + connect(m_killButton, SIGNAL(clicked(bool)), SLOT(on_btnKillMinecraft_clicked())); + + m_launchOfflineButton = new QPushButton(); + horizontalLayout->addWidget(m_launchOfflineButton); + m_launchOfflineButton->setText(tr("Launch Offline")); + updateLaunchButtons(); + connect(m_launchOfflineButton, SIGNAL(clicked(bool)), SLOT(on_btnLaunchMinecraftOffline_clicked())); + + m_closeButton = new QPushButton(); + m_closeButton->setText(tr("Close")); + horizontalLayout->addWidget(m_closeButton); + connect(m_closeButton, SIGNAL(clicked(bool)), SLOT(on_closeButton_clicked())); + + m_container->addButtons(horizontalLayout); + } + + // restore window state + { + auto base64State = APPLICATION->settings()->get("ConsoleWindowState").toByteArray(); + restoreState(QByteArray::fromBase64(base64State)); + auto base64Geometry = APPLICATION->settings()->get("ConsoleWindowGeometry").toByteArray(); + restoreGeometry(QByteArray::fromBase64(base64Geometry)); + } + + // set up instance and launch process recognition + { + auto launchTask = m_instance->getLaunchTask(); + on_InstanceLaunchTask_changed(launchTask); + connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, &InstanceWindow::on_InstanceLaunchTask_changed); + connect(m_instance.get(), &BaseInstance::runningStatusChanged, this, &InstanceWindow::on_RunningState_changed); + } + + // set up instance destruction detection + { + connect(m_instance.get(), &BaseInstance::statusChanged, this, &InstanceWindow::on_instanceStatusChanged); + } + show(); +} + +void InstanceWindow::on_instanceStatusChanged(BaseInstance::Status, BaseInstance::Status newStatus) +{ + if(newStatus == BaseInstance::Status::Gone) + { + m_doNotSave = true; + close(); + } +} + +void InstanceWindow::updateLaunchButtons() +{ + if(m_instance->isRunning()) + { + m_launchOfflineButton->setEnabled(false); + m_killButton->setText(tr("Kill")); + m_killButton->setObjectName("killButton"); + m_killButton->setToolTip(tr("Kill the running instance")); + } + else if(!m_instance->canLaunch()) + { + m_launchOfflineButton->setEnabled(false); + m_killButton->setText(tr("Launch")); + m_killButton->setObjectName("launchButton"); + m_killButton->setToolTip(tr("Launch the instance")); + m_killButton->setEnabled(false); + } + else + { + m_launchOfflineButton->setEnabled(true); + m_killButton->setText(tr("Launch")); + m_killButton->setObjectName("launchButton"); + m_killButton->setToolTip(tr("Launch the instance")); + } + // NOTE: this is a hack to force the button to recalculate its style + m_killButton->setStyleSheet("/* */"); + m_killButton->setStyleSheet(QString()); +} + +void InstanceWindow::on_btnLaunchMinecraftOffline_clicked() +{ + APPLICATION->launch(m_instance, false, nullptr); +} + +void InstanceWindow::on_InstanceLaunchTask_changed(shared_qobject_ptr proc) +{ + m_proc = proc; +} + +void InstanceWindow::on_RunningState_changed(bool running) +{ + updateLaunchButtons(); + m_container->refreshContainer(); + if(running) { + selectPage("log"); + } +} + +void InstanceWindow::on_closeButton_clicked() +{ + close(); +} + +void InstanceWindow::closeEvent(QCloseEvent *event) +{ + bool proceed = true; + if(!m_doNotSave) + { + proceed &= m_container->prepareToClose(); + } + + if(!proceed) + { + return; + } + + APPLICATION->settings()->set("ConsoleWindowState", saveState().toBase64()); + APPLICATION->settings()->set("ConsoleWindowGeometry", saveGeometry().toBase64()); + emit isClosing(); + event->accept(); +} + +bool InstanceWindow::saveAll() +{ + return m_container->saveAll(); +} + +void InstanceWindow::on_btnKillMinecraft_clicked() +{ + if(m_instance->isRunning()) + { + APPLICATION->kill(m_instance); + } + else + { + APPLICATION->launch(m_instance, true, nullptr); + } +} + +QString InstanceWindow::instanceId() +{ + return m_instance->id(); +} + +bool InstanceWindow::selectPage(QString pageId) +{ + return m_container->selectPage(pageId); +} + +void InstanceWindow::refreshContainer() +{ + m_container->refreshContainer(); +} + +InstanceWindow::~InstanceWindow() +{ +} + +bool InstanceWindow::requestClose() +{ + if(m_container->prepareToClose()) + { + close(); + return true; + } + return false; +} diff --git a/ultimmc/launcher/ui/InstanceWindow.h b/ultimmc/launcher/ui/InstanceWindow.h new file mode 100644 index 0000000..1acf684 --- /dev/null +++ b/ultimmc/launcher/ui/InstanceWindow.h @@ -0,0 +1,76 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "LaunchController.h" +#include "launch/LaunchTask.h" + +#include "ui/pages/BasePageContainer.h" + +#include "QObjectPtr.h" + +class QPushButton; +class PageContainer; +class InstanceWindow : public QMainWindow, public BasePageContainer +{ + Q_OBJECT + +public: + explicit InstanceWindow(InstancePtr proc, QWidget *parent = 0); + virtual ~InstanceWindow(); + + bool selectPage(QString pageId) override; + void refreshContainer() override; + + QString instanceId(); + + // save all settings and changes (prepare for launch) + bool saveAll(); + + // request closing the window (from a page) + bool requestClose() override; + +signals: + void isClosing(); + +private +slots: + void on_closeButton_clicked(); + void on_btnKillMinecraft_clicked(); + void on_btnLaunchMinecraftOffline_clicked(); + + void on_InstanceLaunchTask_changed(shared_qobject_ptr proc); + void on_RunningState_changed(bool running); + void on_instanceStatusChanged(BaseInstance::Status, BaseInstance::Status newStatus); + +protected: + void closeEvent(QCloseEvent *) override; + +private: + void updateLaunchButtons(); + +private: + shared_qobject_ptr m_proc; + InstancePtr m_instance; + bool m_doNotSave = false; + PageContainer *m_container = nullptr; + QPushButton *m_closeButton = nullptr; + QPushButton *m_killButton = nullptr; + QPushButton *m_launchOfflineButton = nullptr; +}; diff --git a/ultimmc/launcher/ui/MainWindow.cpp b/ultimmc/launcher/ui/MainWindow.cpp new file mode 100644 index 0000000..0a74773 --- /dev/null +++ b/ultimmc/launcher/ui/MainWindow.cpp @@ -0,0 +1,2042 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Authors: Andrew Okin + * Peterix + * Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "Application.h" +#include "BuildConfig.h" + +#include "MainWindow.h" + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "InstanceWindow.h" +#include "InstancePageProvider.h" +#include "JavaCommon.h" +#include "LaunchController.h" + +#include "ui/instanceview/InstanceProxyModel.h" +#include "ui/instanceview/InstanceView.h" +#include "ui/instanceview/InstanceDelegate.h" +#include "ui/widgets/LabeledToolButton.h" +#include "ui/dialogs/NewInstanceDialog.h" +#include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/AboutDialog.h" +#include "ui/dialogs/VersionSelectDialog.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/IconPickerDialog.h" +#include "ui/dialogs/CopyInstanceDialog.h" +#include "ui/dialogs/UpdateDialog.h" +#include "ui/dialogs/EditAccountDialog.h" +#include "ui/dialogs/NotificationDialog.h" +#include "ui/dialogs/CreateShortcutDialog.h" +#include "ui/dialogs/ExportInstanceDialog.h" +#include "ui/dialogs/ModrinthExportDialog.h" + +#include "UpdateController.h" +#include "KonamiCode.h" + +#include "InstanceImportTask.h" +#include "InstanceCopyTask.h" + +#include "MMCTime.h" + +namespace { +QString profileInUseFilter(const QString & profile, bool used) +{ + if(used) + { + return QObject::tr("%1 (in use)").arg(profile); + } + else + { + return profile; + } +} +} + +// WHY: to hold the pre-translation strings together with the T pointer, so it can be retranslated without a lot of ugly code +template +class Translated +{ +public: + Translated(){} + Translated(QWidget *parent) + { + m_contained = new T(parent); + } + void setTooltipId(const char * tooltip) + { + m_tooltip = tooltip; + } + void setTextId(const char * text) + { + m_text = text; + } + operator T*() + { + return m_contained; + } + T * operator->() + { + return m_contained; + } + void retranslate() + { + if(m_text) + { + QString result; + result = QApplication::translate("MainWindow", m_text); + if(result.contains("%1")) { + result = result.arg(BuildConfig.LAUNCHER_NAME); + } + m_contained->setText(result); + } + if(m_tooltip) + { + QString result; + result = QApplication::translate("MainWindow", m_tooltip); + if(result.contains("%1")) { + result = result.arg(BuildConfig.LAUNCHER_NAME); + } + m_contained->setToolTip(result); + } + } +private: + T * m_contained = nullptr; + const char * m_text = nullptr; + const char * m_tooltip = nullptr; +}; +using TranslatedAction = Translated; +using TranslatedToolButton = Translated; + +class TranslatedToolbar +{ +public: + TranslatedToolbar(){} + TranslatedToolbar(QWidget *parent) + { + m_contained = new QToolBar(parent); + } + void setWindowTitleId(const char * title) + { + m_title = title; + } + operator QToolBar*() + { + return m_contained; + } + QToolBar * operator->() + { + return m_contained; + } + void retranslate() + { + if(m_title) + { + m_contained->setWindowTitle(QApplication::translate("MainWindow", m_title)); + } + } +private: + QToolBar * m_contained = nullptr; + const char * m_title = nullptr; +}; + +class MainWindow::Ui +{ +public: + TranslatedAction actionAddInstance; + //TranslatedAction actionRefresh; + TranslatedAction actionCheckUpdate; + TranslatedAction actionSettings; + TranslatedAction actionPatreon; + TranslatedAction actionMoreNews; + TranslatedAction actionManageAccounts; + TranslatedAction actionLaunchInstance; + TranslatedAction actionRenameInstance; + TranslatedAction actionChangeInstGroup; + TranslatedAction actionChangeInstIcon; + TranslatedAction actionEditInstNotes; + TranslatedAction actionEditInstance; + TranslatedAction actionWorlds; + TranslatedAction actionMods; + TranslatedAction actionViewSelectedInstFolder; + TranslatedAction actionViewSelectedMCFolder; + TranslatedAction actionViewSelectedModsFolder; + TranslatedAction actionDeleteInstance; + TranslatedAction actionConfig_Folder; + TranslatedAction actionCAT; + TranslatedAction actionCopyInstance; + TranslatedAction actionLaunchInstanceOffline; + TranslatedAction actionScreenshots; + TranslatedAction actionExportInstance; + TranslatedAction actionCreateShortcut; + QVector all_actions; + + LabeledToolButton *renameButton = nullptr; + LabeledToolButton *changeIconButton = nullptr; + + QMenu * foldersMenu = nullptr; + TranslatedToolButton foldersMenuButton; + TranslatedAction actionViewInstanceFolder; + TranslatedAction actionViewCentralModsFolder; + + QMenu * helpMenu = nullptr; + TranslatedToolButton helpMenuButton; + TranslatedAction actionReportBug; + TranslatedAction actionDISCORD; + TranslatedAction actionREDDIT; + TranslatedAction actionAbout; + + QVector all_toolbuttons; + + QWidget *centralWidget = nullptr; + QHBoxLayout *horizontalLayout = nullptr; + QStatusBar *statusBar = nullptr; + + TranslatedToolbar mainToolBar; + TranslatedToolbar instanceToolBar; + TranslatedToolbar newsToolBar; + QVector all_toolbars; + bool m_kill = false; + + void updateLaunchAction() + { + if(m_kill) + { + actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Kill")); + actionLaunchInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Kill the running instance")); + } + else + { + actionLaunchInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch")); + actionLaunchInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance.")); + } + actionLaunchInstance.retranslate(); + } + void setLaunchAction(bool kill) + { + m_kill = kill; + updateLaunchAction(); + } + + void createMainToolbar(QMainWindow *MainWindow) + { + mainToolBar = TranslatedToolbar(MainWindow); + mainToolBar->setObjectName(QStringLiteral("mainToolBar")); + mainToolBar->setMovable(false); + mainToolBar->setAllowedAreas(Qt::TopToolBarArea); + mainToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + mainToolBar->setFloatable(false); + mainToolBar.setWindowTitleId(QT_TRANSLATE_NOOP("MainWindow", "Main Toolbar")); + + actionAddInstance = TranslatedAction(MainWindow); + actionAddInstance->setObjectName(QStringLiteral("actionAddInstance")); + actionAddInstance->setIcon(APPLICATION->getThemedIcon("new")); + actionAddInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Add Instance")); + actionAddInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Add a new instance.")); + all_actions.append(&actionAddInstance); + mainToolBar->addAction(actionAddInstance); + + mainToolBar->addSeparator(); + + foldersMenu = new QMenu(MainWindow); + foldersMenu->setToolTipsVisible(true); + + actionViewInstanceFolder = TranslatedAction(MainWindow); + actionViewInstanceFolder->setObjectName(QStringLiteral("actionViewInstanceFolder")); + actionViewInstanceFolder->setIcon(APPLICATION->getThemedIcon("viewfolder")); + actionViewInstanceFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Instance Folder")); + actionViewInstanceFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the instance folder in a file browser.")); + all_actions.append(&actionViewInstanceFolder); + foldersMenu->addAction(actionViewInstanceFolder); + + actionViewCentralModsFolder = TranslatedAction(MainWindow); + actionViewCentralModsFolder->setObjectName(QStringLiteral("actionViewCentralModsFolder")); + actionViewCentralModsFolder->setIcon(APPLICATION->getThemedIcon("centralmods")); + actionViewCentralModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Central Mods Folder")); + actionViewCentralModsFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the central mods folder in a file browser.")); + all_actions.append(&actionViewCentralModsFolder); + foldersMenu->addAction(actionViewCentralModsFolder); + + foldersMenuButton = TranslatedToolButton(MainWindow); + foldersMenuButton.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Folders")); + foldersMenuButton.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open one of the folders shared between instances.")); + foldersMenuButton->setMenu(foldersMenu); + foldersMenuButton->setPopupMode(QToolButton::InstantPopup); + foldersMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + foldersMenuButton->setIcon(APPLICATION->getThemedIcon("viewfolder")); + foldersMenuButton->setFocusPolicy(Qt::NoFocus); + all_toolbuttons.append(&foldersMenuButton); + QWidgetAction* foldersButtonAction = new QWidgetAction(MainWindow); + foldersButtonAction->setDefaultWidget(foldersMenuButton); + mainToolBar->addAction(foldersButtonAction); + + actionSettings = TranslatedAction(MainWindow); + actionSettings->setObjectName(QStringLiteral("actionSettings")); + actionSettings->setIcon(APPLICATION->getThemedIcon("settings")); + actionSettings->setMenuRole(QAction::PreferencesRole); + actionSettings.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Settings")); + actionSettings.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change settings.")); + all_actions.append(&actionSettings); + mainToolBar->addAction(actionSettings); + + helpMenu = new QMenu(MainWindow); + helpMenu->setToolTipsVisible(true); + + if (!BuildConfig.BUG_TRACKER_URL.isEmpty()) { + actionReportBug = TranslatedAction(MainWindow); + actionReportBug->setObjectName(QStringLiteral("actionReportBug")); + actionReportBug->setIcon(APPLICATION->getThemedIcon("bug")); + actionReportBug.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Report a Bug")); + actionReportBug.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the bug tracker to report a bug with %1.")); + all_actions.append(&actionReportBug); + helpMenu->addAction(actionReportBug); + } + + if (!BuildConfig.DISCORD_URL.isEmpty()) { + actionDISCORD = TranslatedAction(MainWindow); + actionDISCORD->setObjectName(QStringLiteral("actionDISCORD")); + actionDISCORD->setIcon(APPLICATION->getThemedIcon("discord")); + actionDISCORD.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Discord")); + actionDISCORD.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 discord voice chat.")); + all_actions.append(&actionDISCORD); + helpMenu->addAction(actionDISCORD); + } + + if (!BuildConfig.SUBREDDIT_URL.isEmpty()) { + actionREDDIT = TranslatedAction(MainWindow); + actionREDDIT->setObjectName(QStringLiteral("actionREDDIT")); + actionREDDIT->setIcon(APPLICATION->getThemedIcon("reddit-alien")); + actionREDDIT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Reddit")); + actionREDDIT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open %1 subreddit.")); + all_actions.append(&actionREDDIT); + helpMenu->addAction(actionREDDIT); + } + + actionAbout = TranslatedAction(MainWindow); + actionAbout->setObjectName(QStringLiteral("actionAbout")); + actionAbout->setIcon(APPLICATION->getThemedIcon("about")); + actionAbout->setMenuRole(QAction::AboutRole); + actionAbout.setTextId(QT_TRANSLATE_NOOP("MainWindow", "About %1")); + actionAbout.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View information about %1.")); + all_actions.append(&actionAbout); + helpMenu->addAction(actionAbout); + + helpMenuButton = TranslatedToolButton(MainWindow); + helpMenuButton.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Help")); + helpMenuButton.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Get help with %1 or Minecraft.")); + helpMenuButton->setMenu(helpMenu); + helpMenuButton->setPopupMode(QToolButton::InstantPopup); + helpMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + helpMenuButton->setIcon(APPLICATION->getThemedIcon("help")); + helpMenuButton->setFocusPolicy(Qt::NoFocus); + all_toolbuttons.append(&helpMenuButton); + QWidgetAction* helpButtonAction = new QWidgetAction(MainWindow); + helpButtonAction->setDefaultWidget(helpMenuButton); + mainToolBar->addAction(helpButtonAction); + + if(BuildConfig.UPDATER_ENABLED) + { + actionCheckUpdate = TranslatedAction(MainWindow); + actionCheckUpdate->setObjectName(QStringLiteral("actionCheckUpdate")); + actionCheckUpdate->setIcon(APPLICATION->getThemedIcon("checkupdate")); + actionCheckUpdate.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Update")); + actionCheckUpdate.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Check for new updates for %1.")); + all_actions.append(&actionCheckUpdate); + mainToolBar->addAction(actionCheckUpdate); + } + + mainToolBar->addSeparator(); + + actionPatreon = TranslatedAction(MainWindow); + actionPatreon->setObjectName(QStringLiteral("actionPatreon")); + actionPatreon->setIcon(APPLICATION->getThemedIcon("patreon")); + actionPatreon.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Support %1")); + actionPatreon.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the %1 Patreon page.")); + all_actions.append(&actionPatreon); + mainToolBar->addAction(actionPatreon); + + actionCAT = TranslatedAction(MainWindow); + actionCAT->setObjectName(QStringLiteral("actionCAT")); + actionCAT->setCheckable(true); + actionCAT->setIcon(APPLICATION->getThemedIcon("cat")); + actionCAT.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Meow")); + actionCAT.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "It's a fluffy kitty :3")); + actionCAT->setPriority(QAction::LowPriority); + all_actions.append(&actionCAT); + mainToolBar->addAction(actionCAT); + + // profile menu and its actions + actionManageAccounts = TranslatedAction(MainWindow); + actionManageAccounts->setObjectName(QStringLiteral("actionManageAccounts")); + actionManageAccounts.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Manage Accounts")); + // FIXME: no tooltip! + actionManageAccounts->setCheckable(false); + actionManageAccounts->setIcon(APPLICATION->getThemedIcon("accounts")); + all_actions.append(&actionManageAccounts); + + all_toolbars.append(&mainToolBar); + MainWindow->addToolBar(Qt::TopToolBarArea, mainToolBar); + } + + void createStatusBar(QMainWindow *MainWindow) + { + statusBar = new QStatusBar(MainWindow); + statusBar->setObjectName(QStringLiteral("statusBar")); + MainWindow->setStatusBar(statusBar); + } + + void createNewsToolbar(QMainWindow *MainWindow) + { + newsToolBar = TranslatedToolbar(MainWindow); + newsToolBar->setObjectName(QStringLiteral("newsToolBar")); + newsToolBar->setMovable(false); + newsToolBar->setAllowedAreas(Qt::BottomToolBarArea); + newsToolBar->setIconSize(QSize(16, 16)); + newsToolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + newsToolBar->setFloatable(false); + newsToolBar->setWindowTitle(QT_TRANSLATE_NOOP("MainWindow", "News Toolbar")); + + actionMoreNews = TranslatedAction(MainWindow); + actionMoreNews->setObjectName(QStringLiteral("actionMoreNews")); + actionMoreNews->setIcon(APPLICATION->getThemedIcon("news")); + actionMoreNews.setTextId(QT_TRANSLATE_NOOP("MainWindow", "More news...")); + actionMoreNews.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the development blog to read more news about %1.")); + all_actions.append(&actionMoreNews); + newsToolBar->addAction(actionMoreNews); + + all_toolbars.append(&newsToolBar); + MainWindow->addToolBar(Qt::BottomToolBarArea, newsToolBar); + } + + void createInstanceToolbar(QMainWindow *MainWindow) + { + instanceToolBar = TranslatedToolbar(MainWindow); + instanceToolBar->setObjectName(QStringLiteral("instanceToolBar")); + // disabled until we have an instance selected + instanceToolBar->setEnabled(false); + instanceToolBar->setAllowedAreas(Qt::LeftToolBarArea | Qt::RightToolBarArea); + instanceToolBar->setToolButtonStyle(Qt::ToolButtonTextOnly); + instanceToolBar->setFloatable(false); + instanceToolBar->setWindowTitle(QT_TRANSLATE_NOOP("MainWindow", "Instance Toolbar")); + + // NOTE: not added to toolbar, but used for instance context menu (right click) + actionChangeInstIcon = TranslatedAction(MainWindow); + actionChangeInstIcon->setObjectName(QStringLiteral("actionChangeInstIcon")); + actionChangeInstIcon->setIcon(QIcon(":/icons/instances/grass")); + actionChangeInstIcon->setIconVisibleInMenu(true); + actionChangeInstIcon.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Change Icon")); + actionChangeInstIcon.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the selected instance's icon.")); + all_actions.append(&actionChangeInstIcon); + + changeIconButton = new LabeledToolButton(MainWindow); + changeIconButton->setObjectName(QStringLiteral("changeIconButton")); + changeIconButton->setIcon(APPLICATION->getThemedIcon("news")); + changeIconButton->setToolTip(actionChangeInstIcon->toolTip()); + changeIconButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + instanceToolBar->addWidget(changeIconButton); + + // NOTE: not added to toolbar, but used for instance context menu (right click) + actionRenameInstance = TranslatedAction(MainWindow); + actionRenameInstance->setObjectName(QStringLiteral("actionRenameInstance")); + actionRenameInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Rename")); + actionRenameInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Rename the selected instance.")); + all_actions.append(&actionRenameInstance); + + // the rename label is inside the rename tool button + renameButton = new LabeledToolButton(MainWindow); + renameButton->setObjectName(QStringLiteral("renameButton")); + renameButton->setToolTip(actionRenameInstance->toolTip()); + renameButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + instanceToolBar->addWidget(renameButton); + + instanceToolBar->addSeparator(); + + actionLaunchInstance = TranslatedAction(MainWindow); + actionLaunchInstance->setObjectName(QStringLiteral("actionLaunchInstance")); + all_actions.append(&actionLaunchInstance); + instanceToolBar->addAction(actionLaunchInstance); + + actionLaunchInstanceOffline = TranslatedAction(MainWindow); + actionLaunchInstanceOffline->setObjectName(QStringLiteral("actionLaunchInstanceOffline")); + actionLaunchInstanceOffline.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Launch Offline")); + actionLaunchInstanceOffline.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Launch the selected instance in offline mode.")); + all_actions.append(&actionLaunchInstanceOffline); + instanceToolBar->addAction(actionLaunchInstanceOffline); + + instanceToolBar->addSeparator(); + + actionEditInstance = TranslatedAction(MainWindow); + actionEditInstance->setObjectName(QStringLiteral("actionEditInstance")); + actionEditInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Edit Instance")); + actionEditInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the instance settings, mods and versions.")); + all_actions.append(&actionEditInstance); + instanceToolBar->addAction(actionEditInstance); + + actionEditInstNotes = TranslatedAction(MainWindow); + actionEditInstNotes->setObjectName(QStringLiteral("actionEditInstNotes")); + actionEditInstNotes.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Edit Notes")); + actionEditInstNotes.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Edit the notes for the selected instance.")); + all_actions.append(&actionEditInstNotes); + instanceToolBar->addAction(actionEditInstNotes); + + actionMods = TranslatedAction(MainWindow); + actionMods->setObjectName(QStringLiteral("actionMods")); + actionMods.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Mods")); + actionMods.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View the mods of this instance.")); + all_actions.append(&actionMods); + instanceToolBar->addAction(actionMods); + + actionWorlds = TranslatedAction(MainWindow); + actionWorlds->setObjectName(QStringLiteral("actionWorlds")); + actionWorlds.setTextId(QT_TRANSLATE_NOOP("MainWindow", "View Worlds")); + actionWorlds.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View the worlds of this instance.")); + all_actions.append(&actionWorlds); + instanceToolBar->addAction(actionWorlds); + + actionScreenshots = TranslatedAction(MainWindow); + actionScreenshots->setObjectName(QStringLiteral("actionScreenshots")); + actionScreenshots.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Manage Screenshots")); + actionScreenshots.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "View and upload screenshots for this instance.")); + all_actions.append(&actionScreenshots); + instanceToolBar->addAction(actionScreenshots); + + actionChangeInstGroup = TranslatedAction(MainWindow); + actionChangeInstGroup->setObjectName(QStringLiteral("actionChangeInstGroup")); + actionChangeInstGroup.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Change Group")); + actionChangeInstGroup.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Change the selected instance's group.")); + all_actions.append(&actionChangeInstGroup); + instanceToolBar->addAction(actionChangeInstGroup); + + instanceToolBar->addSeparator(); + + actionViewSelectedMCFolder = TranslatedAction(MainWindow); + actionViewSelectedMCFolder->setObjectName(QStringLiteral("actionViewSelectedMCFolder")); + actionViewSelectedMCFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Minecraft Folder")); + actionViewSelectedMCFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's minecraft folder in a file browser.")); + all_actions.append(&actionViewSelectedMCFolder); + instanceToolBar->addAction(actionViewSelectedMCFolder); + + /* + actionViewSelectedModsFolder = TranslatedAction(MainWindow); + actionViewSelectedModsFolder->setObjectName(QStringLiteral("actionViewSelectedModsFolder")); + actionViewSelectedModsFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Mods Folder")); + actionViewSelectedModsFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's mods folder in a file browser.")); + all_actions.append(&actionViewSelectedModsFolder); + instanceToolBar->addAction(actionViewSelectedModsFolder); + */ + + actionConfig_Folder = TranslatedAction(MainWindow); + actionConfig_Folder->setObjectName(QStringLiteral("actionConfig_Folder")); + actionConfig_Folder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Config Folder")); + actionConfig_Folder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the instance's config folder.")); + all_actions.append(&actionConfig_Folder); + instanceToolBar->addAction(actionConfig_Folder); + + actionViewSelectedInstFolder = TranslatedAction(MainWindow); + actionViewSelectedInstFolder->setObjectName(QStringLiteral("actionViewSelectedInstFolder")); + actionViewSelectedInstFolder.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Instance Folder")); + actionViewSelectedInstFolder.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Open the selected instance's root folder in a file browser.")); + all_actions.append(&actionViewSelectedInstFolder); + instanceToolBar->addAction(actionViewSelectedInstFolder); + + instanceToolBar->addSeparator(); + + actionCreateShortcut = TranslatedAction(MainWindow); + actionCreateShortcut->setObjectName(QStringLiteral("actionCreateShortcut")); + actionCreateShortcut.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Create Shortcut")); + actionCreateShortcut.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Create a shortcut that launches the selected instance")); + all_actions.append(&actionCreateShortcut); + instanceToolBar->addAction(actionCreateShortcut); + + actionExportInstance = TranslatedAction(MainWindow); + actionExportInstance->setObjectName(QStringLiteral("actionExportInstance")); + actionExportInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Export Instance")); + actionExportInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Export the selected instance as a zip file.")); + all_actions.append(&actionExportInstance); + instanceToolBar->addAction(actionExportInstance); + + actionDeleteInstance = TranslatedAction(MainWindow); + actionDeleteInstance->setObjectName(QStringLiteral("actionDeleteInstance")); + actionDeleteInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Delete")); + actionDeleteInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Delete the selected instance.")); + all_actions.append(&actionDeleteInstance); + instanceToolBar->addAction(actionDeleteInstance); + + actionCopyInstance = TranslatedAction(MainWindow); + actionCopyInstance->setObjectName(QStringLiteral("actionCopyInstance")); + actionCopyInstance->setIcon(APPLICATION->getThemedIcon("copy")); + actionCopyInstance.setTextId(QT_TRANSLATE_NOOP("MainWindow", "Copy Instance")); + actionCopyInstance.setTooltipId(QT_TRANSLATE_NOOP("MainWindow", "Copy the selected instance.")); + all_actions.append(&actionCopyInstance); + instanceToolBar->addAction(actionCopyInstance); + + all_toolbars.append(&instanceToolBar); + MainWindow->addToolBar(Qt::RightToolBarArea, instanceToolBar); + } + + void setupUi(QMainWindow *MainWindow) + { + if (MainWindow->objectName().isEmpty()) + { + MainWindow->setObjectName(QStringLiteral("MainWindow")); + } + MainWindow->resize(800, 600); + MainWindow->setWindowIcon(APPLICATION->getThemedIcon("logo")); + MainWindow->setWindowTitle(BuildConfig.LAUNCHER_DISPLAYNAME); +#ifndef QT_NO_ACCESSIBILITY + MainWindow->setAccessibleName(BuildConfig.LAUNCHER_NAME); +#endif + + createMainToolbar(MainWindow); + + centralWidget = new QWidget(MainWindow); + centralWidget->setObjectName(QStringLiteral("centralWidget")); + horizontalLayout = new QHBoxLayout(centralWidget); + horizontalLayout->setSpacing(0); + horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); + horizontalLayout->setSizeConstraint(QLayout::SetDefaultConstraint); + horizontalLayout->setContentsMargins(0, 0, 0, 0); + MainWindow->setCentralWidget(centralWidget); + + createStatusBar(MainWindow); + createNewsToolbar(MainWindow); + createInstanceToolbar(MainWindow); + + retranslateUi(MainWindow); + + QMetaObject::connectSlotsByName(MainWindow); + } // setupUi + + void retranslateUi(QMainWindow *MainWindow) + { + QString winTitle = tr("%1 - Version %2", "Launcher - Version X").arg(BuildConfig.LAUNCHER_DISPLAYNAME, BuildConfig.printableVersionString()); + if (!BuildConfig.BUILD_PLATFORM.isEmpty()) + { + winTitle += tr(" on %1", "on platform, as in operating system").arg(BuildConfig.BUILD_PLATFORM); + } + MainWindow->setWindowTitle(winTitle); + // all the actions + for(auto * item: all_actions) + { + item->retranslate(); + } + for(auto * item: all_toolbars) + { + item->retranslate(); + } + for(auto * item: all_toolbuttons) + { + item->retranslate(); + } + // submenu buttons + foldersMenuButton->setText(tr("Folders")); + helpMenuButton->setText(tr("Help")); + } // retranslateUi +}; + +MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new MainWindow::Ui) +{ + ui->setupUi(this); + + // OSX magic. + setUnifiedTitleAndToolBarOnMac(true); + + // Global shortcuts + { + // FIXME: This is kinda weird. and bad. We need some kind of managed shutdown. + auto q = new QShortcut(QKeySequence::Quit, this); + connect(q, SIGNAL(activated()), qApp, SLOT(quit())); + } + + // Konami Code + { + secretEventFilter = new KonamiCode(this); + connect(secretEventFilter, &KonamiCode::triggered, this, &MainWindow::konamiTriggered); + } + + // Add the news label to the news toolbar. + { + m_newsChecker.reset(new NewsChecker(APPLICATION->network(), BuildConfig.NEWS_RSS_URL)); + newsLabel = new QToolButton(); + newsLabel->setIcon(APPLICATION->getThemedIcon("news")); + newsLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + newsLabel->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + newsLabel->setFocusPolicy(Qt::NoFocus); + ui->newsToolBar->insertWidget(ui->actionMoreNews, newsLabel); + QObject::connect(newsLabel, &QAbstractButton::clicked, this, &MainWindow::newsButtonClicked); + QObject::connect(m_newsChecker.get(), &NewsChecker::newsLoaded, this, &MainWindow::updateNewsLabel); + updateNewsLabel(); + } + + // Create the instance list widget + { + view = new InstanceView(ui->centralWidget); + + view->setSelectionMode(QAbstractItemView::SingleSelection); + // FIXME: leaks ListViewDelegate + view->setItemDelegate(new ListViewDelegate(this)); + view->setFrameShape(QFrame::NoFrame); + // do not show ugly blue border on the mac + view->setAttribute(Qt::WA_MacShowFocusRect, false); + + view->installEventFilter(this); + view->setContextMenuPolicy(Qt::CustomContextMenu); + connect(view, &QWidget::customContextMenuRequested, this, &MainWindow::showInstanceContextMenu); + connect(view, &InstanceView::droppedURLs, this, &MainWindow::droppedURLs, Qt::QueuedConnection); + + proxymodel = new InstanceProxyModel(this); + proxymodel->setSourceModel(APPLICATION->instances().get()); + proxymodel->sort(0); + connect(proxymodel, &InstanceProxyModel::dataChanged, this, &MainWindow::instanceDataChanged); + + view->setModel(proxymodel); + view->setSourceOfGroupCollapseStatus([](const QString & groupName)->bool { + return APPLICATION->instances()->isGroupCollapsed(groupName); + }); + connect(view, &InstanceView::groupStateChanged, APPLICATION->instances().get(), &InstanceList::on_GroupStateChanged); + ui->horizontalLayout->addWidget(view); + } + // The cat background + { + bool cat_enable = APPLICATION->settings()->get("TheCat").toBool(); + ui->actionCAT->setChecked(cat_enable); + // NOTE: calling the operator like that is an ugly hack to appease ancient gcc... + connect(ui->actionCAT.operator->(), SIGNAL(toggled(bool)), SLOT(onCatToggled(bool))); + setCatBackground(cat_enable); + } + // start instance when double-clicked + connect(view, &InstanceView::activated, this, &MainWindow::instanceActivated); + + // track the selection -- update the instance toolbar + connect(view->selectionModel(), &QItemSelectionModel::currentChanged, this, &MainWindow::instanceChanged); + + // track icon changes and update the toolbar! + connect(APPLICATION->icons().get(), &IconList::iconUpdated, this, &MainWindow::iconUpdated); + + // model reset -> selection is invalid. All the instance pointers are wrong. + connect(APPLICATION->instances().get(), &InstanceList::dataIsInvalid, this, &MainWindow::selectionBad); + + // handle newly added instances + connect(APPLICATION->instances().get(), &InstanceList::instanceSelectRequest, this, &MainWindow::instanceSelectRequest); + + // When the global settings page closes, we want to know about it and update our state + connect(APPLICATION, &Application::globalSettingsClosed, this, &MainWindow::globalSettingsClosed); + + m_statusLeft = new QLabel(tr("No instance selected"), this); + m_statusCenter = new QLabel(tr("Total playtime: 0s"), this); + statusBar()->addPermanentWidget(m_statusLeft, 1); + statusBar()->addPermanentWidget(m_statusCenter, 0); + + // Add "manage accounts" button, right align + QWidget *spacer = new QWidget(); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + ui->mainToolBar->addWidget(spacer); + + accountMenu = new QMenu(this); + + repopulateAccountsMenu(); + + accountMenuButton = new QToolButton(this); + accountMenuButton->setMenu(accountMenu); + accountMenuButton->setPopupMode(QToolButton::InstantPopup); + accountMenuButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount")); + + QWidgetAction *accountMenuButtonAction = new QWidgetAction(this); + accountMenuButtonAction->setDefaultWidget(accountMenuButton); + + ui->mainToolBar->addAction(accountMenuButtonAction); + + // Update the menu when the active account changes. + // Shouldn't have to use lambdas here like this, but if I don't, the compiler throws a fit. + // Template hell sucks... + connect( + APPLICATION->accounts().get(), + &AccountList::defaultAccountChanged, + [this] { + defaultAccountChanged(); + } + ); + connect( + APPLICATION->accounts().get(), + &AccountList::listChanged, + [this] + { + repopulateAccountsMenu(); + } + ); + + // Show initial account + defaultAccountChanged(); + + // TODO: refresh accounts here? + // auto accounts = APPLICATION->accounts(); + + // load the news + { + m_newsChecker->reloadNews(); + updateNewsLabel(); + } + + + if(BuildConfig.UPDATER_ENABLED) + { + bool updatesAllowed = APPLICATION->updatesAreAllowed(); + updatesAllowedChanged(updatesAllowed); + + // NOTE: calling the operator like that is an ugly hack to appease ancient gcc... + connect(ui->actionCheckUpdate.operator->(), &QAction::triggered, this, &MainWindow::checkForUpdates); + + // set up the updater object. + auto updater = APPLICATION->updateChecker(); + connect(updater.get(), &UpdateChecker::updateAvailable, this, &MainWindow::updateAvailable); + connect(updater.get(), &UpdateChecker::noUpdateFound, this, &MainWindow::updateNotAvailable); + // if automatic update checks are allowed, start one. + if (APPLICATION->settings()->get("AutoUpdate").toBool() && updatesAllowed) + { + updater->checkForUpdate(false); + } + } + + { + auto checker = new NotificationChecker(); + checker->setNotificationsUrl(QUrl(BuildConfig.NOTIFICATION_URL)); + checker->setApplicationPlatform(BuildConfig.BUILD_PLATFORM); + checker->setApplicationFullVersion(BuildConfig.FULL_VERSION_STR); + m_notificationChecker.reset(checker); + connect(m_notificationChecker.get(), &NotificationChecker::notificationCheckFinished, this, &MainWindow::notificationsChanged); + checker->checkForNotifications(); + } + + setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString()); + + // removing this looks stupid + view->setFocus(); + + retranslateUi(); +} + +void MainWindow::retranslateUi() +{ + auto accounts = APPLICATION->accounts(); + MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); + if(defaultAccount) { + auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); + accountMenuButton->setText(profileLabel); + } + else { + accountMenuButton->setText(tr("Profiles")); + } + + if (m_selectedInstance) { + m_statusLeft->setText(m_selectedInstance->getStatusbarDescription()); + } else { + m_statusLeft->setText(tr("No instance selected")); + } + + ui->retranslateUi(this); +} + +MainWindow::~MainWindow() +{ +} + +QMenu * MainWindow::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction( ui->mainToolBar->toggleViewAction() ); + return filteredMenu; +} + +void MainWindow::konamiTriggered() +{ + qDebug() << "Super Secret Mode ACTIVATED!"; +} + +void MainWindow::showInstanceContextMenu(const QPoint &pos) +{ + QList actions; + + QAction *actionSep = new QAction("", this); + actionSep->setSeparator(true); + + bool onInstance = view->indexAt(pos).isValid(); + if (onInstance) + { + actions = ui->instanceToolBar->actions(); + + // replace the change icon widget with an actual action + actions.replace(0, ui->actionChangeInstIcon); + + // replace the rename widget with an actual action + actions.replace(1, ui->actionRenameInstance); + + // add header + actions.prepend(actionSep); + QAction *actionVoid = new QAction(m_selectedInstance->name(), this); + actionVoid->setEnabled(false); + actions.prepend(actionVoid); + } + else + { + auto group = view->groupNameAt(pos); + + QAction *actionVoid = new QAction(BuildConfig.LAUNCHER_NAME, this); + actionVoid->setEnabled(false); + + QAction *actionCreateInstance = new QAction(tr("Create instance"), this); + actionCreateInstance->setToolTip(ui->actionAddInstance->toolTip()); + if(!group.isNull()) + { + QVariantMap data; + data["group"] = group; + actionCreateInstance->setData(data); + } + + connect(actionCreateInstance, SIGNAL(triggered(bool)), SLOT(on_actionAddInstance_triggered())); + + actions.prepend(actionSep); + actions.prepend(actionVoid); + actions.append(actionCreateInstance); + if(!group.isNull()) + { + QAction *actionDeleteGroup = new QAction(tr("Delete group '%1'").arg(group), this); + QVariantMap data; + data["group"] = group; + actionDeleteGroup->setData(data); + connect(actionDeleteGroup, SIGNAL(triggered(bool)), SLOT(deleteGroup())); + actions.append(actionDeleteGroup); + } + } + QMenu myMenu; + myMenu.addActions(actions); + /* + if (onInstance) + myMenu.setEnabled(m_selectedInstance->canLaunch()); + */ + myMenu.exec(view->mapToGlobal(pos)); +} + +void MainWindow::updateToolsMenu() +{ + QToolButton *exportButton = dynamic_cast(ui->instanceToolBar->widgetForAction(ui->actionExportInstance)); + exportButton->setPopupMode(QToolButton::MenuButtonPopup); + + QMenu *exportMenu = ui->actionExportInstance->menu(); + + if (exportMenu) { + exportMenu->clear(); + } else { + exportMenu = new QMenu(); + } + + exportMenu->addSeparator()->setText(tr("Format")); + + QAction *mmcExport = exportMenu->addAction(BuildConfig.LAUNCHER_NAME); + QAction *modrinthExport = exportMenu->addAction(tr("Modrinth (WIP)")); + + connect(mmcExport, &QAction::triggered, this, &MainWindow::on_actionExportInstance_triggered); + connect(modrinthExport, &QAction::triggered, [this]() + { + if (m_selectedInstance) { + ModrinthExportDialog dlg(m_selectedInstance, this); + dlg.exec(); + } + }); + + ui->actionExportInstance->setMenu(exportMenu); + + QToolButton *launchButton = dynamic_cast(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstance)); + QToolButton *launchOfflineButton = dynamic_cast(ui->instanceToolBar->widgetForAction(ui->actionLaunchInstanceOffline)); + + if(!m_selectedInstance || m_selectedInstance->isRunning()) + { + ui->actionLaunchInstance->setMenu(nullptr); + ui->actionLaunchInstanceOffline->setMenu(nullptr); + launchButton->setPopupMode(QToolButton::InstantPopup); + launchOfflineButton->setPopupMode(QToolButton::InstantPopup); + return; + } + + QMenu *launchMenu = ui->actionLaunchInstance->menu(); + QMenu *launchOfflineMenu = ui->actionLaunchInstanceOffline->menu(); + launchButton->setPopupMode(QToolButton::MenuButtonPopup); + launchOfflineButton->setPopupMode(QToolButton::MenuButtonPopup); + if (launchMenu) + { + launchMenu->clear(); + } + else + { + launchMenu = new QMenu(this); + } + if (launchOfflineMenu) { + launchOfflineMenu->clear(); + } + else + { + launchOfflineMenu = new QMenu(this); + } + + QAction *normalLaunch = launchMenu->addAction(tr("Launch")); + QAction *normalLaunchOffline = launchOfflineMenu->addAction(tr("Launch Offline")); + connect(normalLaunch, &QAction::triggered, [this]() + { + APPLICATION->launch(m_selectedInstance, true); + }); + connect(normalLaunchOffline, &QAction::triggered, [this]() + { + APPLICATION->launch(m_selectedInstance, false); + }); + QString profilersTitle = tr("Profilers"); + launchMenu->addSeparator()->setText(profilersTitle); + launchOfflineMenu->addSeparator()->setText(profilersTitle); + for (auto profiler : APPLICATION->profilers().values()) + { + QAction *profilerAction = launchMenu->addAction(profiler->name()); + QAction *profilerOfflineAction = launchOfflineMenu->addAction(profiler->name()); + QString error; + if (!profiler->check(&error)) + { + profilerAction->setDisabled(true); + profilerOfflineAction->setDisabled(true); + QString profilerToolTip = tr("Profiler not setup correctly. Go into settings, \"External Tools\"."); + profilerAction->setToolTip(profilerToolTip); + profilerOfflineAction->setToolTip(profilerToolTip); + } + else + { + connect(profilerAction, &QAction::triggered, [this, profiler]() + { + APPLICATION->launch(m_selectedInstance, true, profiler.get()); + }); + connect(profilerOfflineAction, &QAction::triggered, [this, profiler]() + { + APPLICATION->launch(m_selectedInstance, false, profiler.get()); + }); + } + } + ui->actionLaunchInstance->setMenu(launchMenu); + ui->actionLaunchInstanceOffline->setMenu(launchOfflineMenu); +} + +void MainWindow::repopulateAccountsMenu() +{ + accountMenu->clear(); + + auto accounts = APPLICATION->accounts(); + MinecraftAccountPtr defaultAccount = accounts->defaultAccount(); + + QString active_profileId = ""; + if (defaultAccount) + { + // this can be called before accountMenuButton exists + if (accountMenuButton) + { + auto profileLabel = profileInUseFilter(defaultAccount->profileName(), defaultAccount->isInUse()); + accountMenuButton->setText(profileLabel); + } + } + + if (accounts->count() <= 0) + { + QAction *action = new QAction(tr("No accounts added!"), this); + action->setEnabled(false); + accountMenu->addAction(action); + } + else + { + // TODO: Nicer way to iterate? + for (int i = 0; i < accounts->count(); i++) + { + MinecraftAccountPtr account = accounts->at(i); + auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); + QAction *action = new QAction(profileLabel, this); + action->setData(i); + action->setCheckable(true); + if (defaultAccount == account) + { + action->setChecked(true); + } + + auto face = account->getFace(); + if(!face.isNull()) { + action->setIcon(face); + } + else { + action->setIcon(APPLICATION->getThemedIcon("noaccount")); + } + accountMenu->addAction(action); + connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); + } + } + + accountMenu->addSeparator(); + + QAction *action = new QAction(tr("No Default Account"), this); + action->setCheckable(true); + action->setIcon(APPLICATION->getThemedIcon("noaccount")); + action->setData(-1); + if (!defaultAccount) { + action->setChecked(true); + } + + accountMenu->addAction(action); + connect(action, SIGNAL(triggered(bool)), SLOT(changeActiveAccount())); + + accountMenu->addSeparator(); + accountMenu->addAction(ui->actionManageAccounts); +} + +void MainWindow::updatesAllowedChanged(bool allowed) +{ + if(!BuildConfig.UPDATER_ENABLED) + { + return; + } + ui->actionCheckUpdate->setEnabled(allowed); +} + +/* + * Assumes the sender is a QAction + */ +void MainWindow::changeActiveAccount() +{ + QAction *sAction = (QAction *)sender(); + + // Profile's associated Mojang username + if (sAction->data().type() != QVariant::Type::Int) + return; + + QVariant data = sAction->data(); + bool valid = false; + int index = data.toInt(&valid); + if(!valid) { + index = -1; + } + auto accounts = APPLICATION->accounts(); + accounts->setDefaultAccount(index == -1 ? nullptr : accounts->at(index)); + defaultAccountChanged(); +} + +void MainWindow::defaultAccountChanged() +{ + repopulateAccountsMenu(); + + MinecraftAccountPtr account = APPLICATION->accounts()->defaultAccount(); + + // FIXME: this needs adjustment for MSA + if (account && account->profileName() != "") + { + auto profileLabel = profileInUseFilter(account->profileName(), account->isInUse()); + accountMenuButton->setText(profileLabel); + auto face = account->getFace(); + if(face.isNull()) { + accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount")); + } + else { + accountMenuButton->setIcon(face); + } + return; + } + + // Set the icon to the "no account" icon. + accountMenuButton->setIcon(APPLICATION->getThemedIcon("noaccount")); + accountMenuButton->setText(tr("Profiles")); +} + +bool MainWindow::eventFilter(QObject *obj, QEvent *ev) +{ + if (obj == view) + { + if (ev->type() == QEvent::KeyPress) + { + secretEventFilter->input(ev); + QKeyEvent *keyEvent = static_cast(ev); + switch (keyEvent->key()) + { + /* + case Qt::Key_Enter: + case Qt::Key_Return: + activateInstance(m_selectedInstance); + return true; + */ + case Qt::Key_Delete: + on_actionDeleteInstance_triggered(); + return true; + case Qt::Key_F5: + refreshInstances(); + return true; + case Qt::Key_F2: + on_actionRenameInstance_triggered(); + return true; + default: + break; + } + } + } + return QMainWindow::eventFilter(obj, ev); +} + +void MainWindow::updateNewsLabel() +{ + if (m_newsChecker->isLoadingNews()) + { + newsLabel->setText(tr("Loading news...")); + newsLabel->setEnabled(false); + } + else + { + QList entries = m_newsChecker->getNewsEntries(); + if (entries.length() > 0) + { + newsLabel->setText(entries[0]->title); + newsLabel->setEnabled(true); + } + else + { + newsLabel->setText(tr("No news available.")); + newsLabel->setEnabled(false); + } + } +} + +void MainWindow::updateAvailable(GoUpdate::Status status) +{ + if(!APPLICATION->updatesAreAllowed()) + { + updateNotAvailable(); + return; + } + UpdateDialog dlg(true, this); + UpdateAction action = (UpdateAction)dlg.exec(); + switch (action) + { + case UPDATE_LATER: + qDebug() << "Update will be installed later."; + break; + case UPDATE_NOW: + downloadUpdates(status); + break; + } +} + +void MainWindow::updateNotAvailable() +{ + UpdateDialog dlg(false, this); + dlg.exec(); +} + +QList stringToIntList(const QString &string) +{ + QStringList split = string.split(',', QString::SkipEmptyParts); + QList out; + for (int i = 0; i < split.size(); ++i) + { + out.append(split.at(i).toInt()); + } + return out; +} +QString intListToString(const QList &list) +{ + QStringList slist; + for (int i = 0; i < list.size(); ++i) + { + slist.append(QString::number(list.at(i))); + } + return slist.join(','); +} +void MainWindow::notificationsChanged() +{ + QList entries = m_notificationChecker->notificationEntries(); + QList shownNotifications = stringToIntList(APPLICATION->settings()->get("ShownNotifications").toString()); + for (auto it = entries.begin(); it != entries.end(); ++it) + { + NotificationChecker::NotificationEntry entry = *it; + if (!shownNotifications.contains(entry.id)) + { + NotificationDialog dialog(entry, this); + if (dialog.exec() == NotificationDialog::DontShowAgain) + { + shownNotifications.append(entry.id); + } + } + } + APPLICATION->settings()->set("ShownNotifications", intListToString(shownNotifications)); +} + +void MainWindow::downloadUpdates(GoUpdate::Status status) +{ + if(!APPLICATION->updatesAreAllowed()) + { + return; + } + qDebug() << "Downloading updates."; + ProgressDialog updateDlg(this); + status.rootPath = APPLICATION->root(); + + auto dlPath = FS::PathCombine(APPLICATION->root(), "update", "XXXXXX"); + if (!FS::ensureFilePathExists(dlPath)) + { + CustomMessageBox::selectable(this, tr("Error"), tr("Couldn't create folder for update downloads:\n%1").arg(dlPath), QMessageBox::Warning)->show(); + } + GoUpdate::DownloadTask updateTask(APPLICATION->network(), status, dlPath, &updateDlg); + // If the task succeeds, install the updates. + if (updateDlg.execWithTask(&updateTask)) + { + /** + * NOTE: This disables launching instances until the update either succeeds (and this process exits) + * or the update fails (and the control leaves this scope). + */ + APPLICATION->updateIsRunning(true); + UpdateController update(this, APPLICATION->root(), updateTask.updateFilesDir(), updateTask.operations()); + update.installUpdates(); + APPLICATION->updateIsRunning(false); + } + else + { + CustomMessageBox::selectable(this, tr("Error"), updateTask.failReason(), QMessageBox::Warning)->show(); + } +} + +void MainWindow::onCatToggled(bool state) +{ + setCatBackground(state); + APPLICATION->settings()->set("TheCat", state); +} + +namespace { +template +T non_stupid_abs(T in) +{ + if (in < 0) + return -in; + return in; +} +} + +void MainWindow::setCatBackground(bool enabled) +{ + if (enabled) + { + QDateTime now = QDateTime::currentDateTime(); + QDateTime birthday(QDate(now.date().year(), 11, 30), QTime(0, 0)); + QDateTime xmas(QDate(now.date().year(), 12, 25), QTime(0, 0)); + QString cat; + if(non_stupid_abs(now.daysTo(xmas)) <= 4) { + cat = "catmas"; + } + else if (non_stupid_abs(now.daysTo(birthday)) <= 12) { + cat = "cattiversary"; + } + else { + cat = "kitteh"; + } + view->setStyleSheet(QString(R"( +InstanceView +{ + background-image: url(:/backgrounds/%1); + background-attachment: fixed; + background-clip: padding; + background-position: top right; + background-repeat: none; + background-color:palette(base); +})").arg(cat)); + } + else + { + view->setStyleSheet(QString()); + } +} + +void MainWindow::runModalTask(Task *task) +{ + connect(task, &Task::failed, [this](QString reason) + { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); + }); + connect(task, &Task::succeeded, [this, task]() + { + QStringList warnings = task->warnings(); + if(warnings.count()) + { + CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } + }); + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(task); +} + +void MainWindow::instanceFromInstanceTask(InstanceTask *rawTask) +{ + unique_qobject_ptr task(APPLICATION->instances()->wrapInstanceTask(rawTask)); + runModalTask(task.get()); +} + +void MainWindow::on_actionCopyInstance_triggered() +{ + if (!m_selectedInstance) + return; + + CopyInstanceDialog copyInstDlg(m_selectedInstance, this); + if (!copyInstDlg.exec()) + return; + + auto copyTask = new InstanceCopyTask(m_selectedInstance, copyInstDlg.shouldCopySaves(), copyInstDlg.shouldKeepPlaytime()); + copyTask->setName(copyInstDlg.instName()); + copyTask->setGroup(copyInstDlg.instGroup()); + copyTask->setIcon(copyInstDlg.iconKey()); + unique_qobject_ptr task(APPLICATION->instances()->wrapInstanceTask(copyTask)); + runModalTask(task.get()); +} + +void MainWindow::finalizeInstance(InstancePtr inst) +{ + view->updateGeometries(); + setSelectedInstanceById(inst->id()); + if (APPLICATION->accounts()->anyAccountIsValid()) + { + ProgressDialog loadDialog(this); + auto update = inst->createUpdateTask(Net::Mode::Online); + connect(update.get(), &Task::failed, [this](QString reason) + { + QString error = QString("Instance load failed: %1").arg(reason); + CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show(); + }); + if(update) + { + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(update.get()); + } + } + else + { + CustomMessageBox::selectable( + this, + tr("Error"), + tr("The launcher cannot download Minecraft or update instances unless you have at least " + "one account added.\nPlease add your Mojang or Minecraft account."), + QMessageBox::Warning + )->show(); + } +} + +void MainWindow::addInstance(QString url) +{ + QString groupName; + do + { + QObject* obj = sender(); + if(!obj) + break; + QAction *action = qobject_cast(obj); + if(!action) + break; + auto map = action->data().toMap(); + if(!map.contains("group")) + break; + groupName = map["group"].toString(); + } while(0); + + if(groupName.isEmpty()) + { + groupName = APPLICATION->settings()->get("LastUsedGroupForNewInstance").toString(); + } + + NewInstanceDialog newInstDlg(groupName, url, this); + if (!newInstDlg.exec()) + return; + + APPLICATION->settings()->set("LastUsedGroupForNewInstance", newInstDlg.instGroup()); + + InstanceTask * creationTask = newInstDlg.extractTask(); + if(creationTask) + { + instanceFromInstanceTask(creationTask); + } +} + +void MainWindow::on_actionAddInstance_triggered() +{ + addInstance(); +} + +void MainWindow::droppedURLs(QList urls) +{ + for(auto & url:urls) + { + if(url.isLocalFile()) + { + addInstance(url.toLocalFile()); + } + else + { + addInstance(url.toString()); + } + // Only process one dropped file... + break; + } +} + +void MainWindow::on_actionREDDIT_triggered() +{ + DesktopServices::openUrl(QUrl(BuildConfig.SUBREDDIT_URL)); +} + +void MainWindow::on_actionDISCORD_triggered() +{ + DesktopServices::openUrl(QUrl(BuildConfig.DISCORD_URL)); +} + +void MainWindow::on_actionChangeInstIcon_triggered() +{ + if (!m_selectedInstance) + return; + + IconPickerDialog dlg(this); + dlg.execWithSelection(m_selectedInstance->iconKey()); + if (dlg.result() == QDialog::Accepted) + { + m_selectedInstance->setIconKey(dlg.selectedIconKey); + auto icon = APPLICATION->icons()->getIcon(dlg.selectedIconKey); + ui->actionChangeInstIcon->setIcon(icon); + ui->changeIconButton->setIcon(icon); + } +} + +void MainWindow::iconUpdated(QString icon) +{ + if (icon == m_currentInstIcon) + { + auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon); + ui->actionChangeInstIcon->setIcon(icon); + ui->changeIconButton->setIcon(icon); + } +} + +void MainWindow::updateInstanceToolIcon(QString new_icon) +{ + m_currentInstIcon = new_icon; + auto icon = APPLICATION->icons()->getIcon(m_currentInstIcon); + ui->actionChangeInstIcon->setIcon(icon); + ui->changeIconButton->setIcon(icon); +} + +void MainWindow::setSelectedInstanceById(const QString &id) +{ + if (id.isNull()) + return; + const QModelIndex index = APPLICATION->instances()->getInstanceIndexById(id); + if (index.isValid()) + { + QModelIndex selectionIndex = proxymodel->mapFromSource(index); + view->selectionModel()->setCurrentIndex(selectionIndex, QItemSelectionModel::ClearAndSelect); + updateStatusCenter(); + } +} + +void MainWindow::on_actionChangeInstGroup_triggered() +{ + if (!m_selectedInstance) + return; + + bool ok = false; + InstanceId instId = m_selectedInstance->id(); + QString name(APPLICATION->instances()->getInstanceGroup(instId)); + auto groups = APPLICATION->instances()->getGroups(); + groups.insert(0, ""); + groups.sort(Qt::CaseInsensitive); + int foo = groups.indexOf(name); + + name = QInputDialog::getItem(this, tr("Group name"), tr("Enter a new group name."), groups, foo, true, &ok); + name = name.simplified(); + if (ok) + { + APPLICATION->instances()->setInstanceGroup(instId, name); + } +} + +void MainWindow::deleteGroup() +{ + QObject* obj = sender(); + if(!obj) + return; + QAction *action = qobject_cast(obj); + if(!action) + return; + auto map = action->data().toMap(); + if(!map.contains("group")) + return; + QString groupName = map["group"].toString(); + if(!groupName.isEmpty()) + { + auto reply = QMessageBox::question(this, tr("Delete group"), tr("Are you sure you want to delete the group %1") + .arg(groupName), QMessageBox::Yes | QMessageBox::No); + if(reply == QMessageBox::Yes) + { + APPLICATION->instances()->deleteGroup(groupName); + } + } +} + +void MainWindow::on_actionViewInstanceFolder_triggered() +{ + QString str = APPLICATION->settings()->get("InstanceDir").toString(); + DesktopServices::openDirectory(str); +} + +void MainWindow::refreshInstances() +{ + APPLICATION->instances()->loadList(); +} + +void MainWindow::on_actionViewCentralModsFolder_triggered() +{ + DesktopServices::openDirectory(APPLICATION->settings()->get("CentralModsDir").toString(), true); +} + +void MainWindow::on_actionConfig_Folder_triggered() +{ + if (m_selectedInstance) + { + QString str = m_selectedInstance->instanceConfigFolder(); + DesktopServices::openDirectory(QDir(str).absolutePath()); + } +} + +void MainWindow::checkForUpdates() +{ + if(BuildConfig.UPDATER_ENABLED) + { + auto updater = APPLICATION->updateChecker(); + updater->checkForUpdate(true); + } + else + { + qWarning() << "Updater not set up. Cannot check for updates."; + } +} + +void MainWindow::on_actionSettings_triggered() +{ + APPLICATION->ShowGlobalSettings(this, "global-settings"); +} + +void MainWindow::globalSettingsClosed() +{ + // FIXME: quick HACK to make this work. improve, optimize. + APPLICATION->instances()->loadList(); + proxymodel->invalidate(); + proxymodel->sort(0); + updateToolsMenu(); + updateStatusCenter(); + update(); +} + +void MainWindow::on_actionInstanceSettings_triggered() +{ + APPLICATION->showInstanceWindow(m_selectedInstance, "settings"); +} + +void MainWindow::on_actionEditInstNotes_triggered() +{ + APPLICATION->showInstanceWindow(m_selectedInstance, "notes"); +} + +void MainWindow::on_actionWorlds_triggered() +{ + APPLICATION->showInstanceWindow(m_selectedInstance, "worlds"); +} + +void MainWindow::on_actionMods_triggered() +{ + APPLICATION->showInstanceWindow(m_selectedInstance, "mods"); +} + +void MainWindow::on_actionEditInstance_triggered() +{ + APPLICATION->showInstanceWindow(m_selectedInstance); +} + +void MainWindow::on_actionScreenshots_triggered() +{ + APPLICATION->showInstanceWindow(m_selectedInstance, "screenshots"); +} + +void MainWindow::on_actionManageAccounts_triggered() +{ + APPLICATION->ShowGlobalSettings(this, "accounts"); +} + +void MainWindow::on_actionReportBug_triggered() +{ + DesktopServices::openUrl(QUrl(BuildConfig.BUG_TRACKER_URL)); +} + +void MainWindow::on_actionPatreon_triggered() +{ + DesktopServices::openUrl(QUrl("https://www.patreon.com/multimc")); +} + +void MainWindow::on_actionMoreNews_triggered() +{ + DesktopServices::openUrl(QUrl("https://multimc.org/posts.html")); +} + +void MainWindow::newsButtonClicked() +{ + QList entries = m_newsChecker->getNewsEntries(); + if (entries.count() > 0) + { + DesktopServices::openUrl(QUrl(entries[0]->link)); + } + else + { + DesktopServices::openUrl(QUrl("https://multimc.org/posts.html")); + } +} + +void MainWindow::on_actionAbout_triggered() +{ + AboutDialog dialog(this); + dialog.exec(); +} + +void MainWindow::on_actionDeleteInstance_triggered() +{ + if (!m_selectedInstance) + { + return; + } + auto id = m_selectedInstance->id(); + auto response = CustomMessageBox::selectable( + this, + tr("CAREFUL!"), + tr("About to delete: %1\nThis is permanent and will completely delete the instance.\n\nAre you sure?").arg(m_selectedInstance->name()), + QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No + )->exec(); + if (response == QMessageBox::Yes) + { + APPLICATION->instances()->deleteInstance(id); + } +} + +void MainWindow::on_actionExportInstance_triggered() +{ + if (m_selectedInstance) + { + ExportInstanceDialog dlg(m_selectedInstance, this); + dlg.exec(); + } +} + +void MainWindow::on_actionRenameInstance_triggered() +{ + if (m_selectedInstance) + { + view->edit(view->currentIndex()); + } +} + +void MainWindow::on_actionViewSelectedInstFolder_triggered() +{ + if (m_selectedInstance) + { + QString str = m_selectedInstance->instanceRoot(); + DesktopServices::openDirectory(QDir(str).absolutePath()); + } +} + +void MainWindow::on_actionViewSelectedMCFolder_triggered() +{ + if (m_selectedInstance) + { + QString str = m_selectedInstance->gameRoot(); + if (!FS::ensureFilePathExists(str)) + { + // TODO: report error + return; + } + DesktopServices::openDirectory(QDir(str).absolutePath()); + } +} + +void MainWindow::on_actionViewSelectedModsFolder_triggered() +{ + if (m_selectedInstance) + { + QString str = m_selectedInstance->modsRoot(); + if (!FS::ensureFilePathExists(str)) + { + // TODO: report error + return; + } + DesktopServices::openDirectory(QDir(str).absolutePath()); + } +} + +void MainWindow::closeEvent(QCloseEvent *event) +{ + // Save the window state and geometry. + APPLICATION->settings()->set("MainWindowState", saveState().toBase64()); + APPLICATION->settings()->set("MainWindowGeometry", saveGeometry().toBase64()); + event->accept(); + emit isClosing(); +} + +void MainWindow::changeEvent(QEvent* event) +{ + if (event->type() == QEvent::LanguageChange) + { + retranslateUi(); + } + QMainWindow::changeEvent(event); +} + +void MainWindow::instanceActivated(QModelIndex index) +{ + if (!index.isValid()) + return; + QString id = index.data(InstanceList::InstanceIDRole).toString(); + InstancePtr inst = APPLICATION->instances()->getInstanceById(id); + if (!inst) + return; + + activateInstance(inst); +} + +void MainWindow::on_actionLaunchInstance_triggered() +{ + if (!m_selectedInstance) + { + return; + } + if(m_selectedInstance->isRunning()) + { + APPLICATION->kill(m_selectedInstance); + } + else + { + APPLICATION->launch(m_selectedInstance); + } +} + +void MainWindow::on_actionCreateShortcut_triggered() { + if (m_selectedInstance) + { + CreateShortcutDialog(this, m_selectedInstance).exec(); + } +} + +void MainWindow::activateInstance(InstancePtr instance) +{ + APPLICATION->launch(instance); +} + +void MainWindow::on_actionLaunchInstanceOffline_triggered() +{ + if (m_selectedInstance) + { + APPLICATION->launch(m_selectedInstance, false); + } +} + +void MainWindow::taskEnd() +{ + QObject *sender = QObject::sender(); + if (sender == m_versionLoadTask) + m_versionLoadTask = NULL; + + sender->deleteLater(); +} + +void MainWindow::startTask(Task *task) +{ + connect(task, SIGNAL(succeeded()), SLOT(taskEnd())); + connect(task, SIGNAL(failed(QString)), SLOT(taskEnd())); + task->start(); +} + +void MainWindow::instanceChanged(const QModelIndex ¤t, const QModelIndex &previous) +{ + if (!current.isValid()) + { + APPLICATION->settings()->set("SelectedInstance", QString()); + selectionBad(); + return; + } + QString id = current.data(InstanceList::InstanceIDRole).toString(); + m_selectedInstance = APPLICATION->instances()->getInstanceById(id); + if (m_selectedInstance) + { + ui->instanceToolBar->setEnabled(true); + if(m_selectedInstance->isRunning()) + { + ui->actionLaunchInstance->setEnabled(true); + ui->setLaunchAction(true); + } + else + { + ui->actionLaunchInstance->setEnabled(m_selectedInstance->canLaunch()); + ui->setLaunchAction(false); + } + ui->actionLaunchInstanceOffline->setEnabled(m_selectedInstance->canLaunch()); + ui->actionExportInstance->setEnabled(m_selectedInstance->canExport()); + ui->renameButton->setText(m_selectedInstance->name()); + m_statusLeft->setText(m_selectedInstance->getStatusbarDescription()); + updateStatusCenter(); + updateInstanceToolIcon(m_selectedInstance->iconKey()); + + updateToolsMenu(); + + APPLICATION->settings()->set("SelectedInstance", m_selectedInstance->id()); + } + else + { + ui->instanceToolBar->setEnabled(false); + APPLICATION->settings()->set("SelectedInstance", QString()); + selectionBad(); + return; + } +} + +void MainWindow::instanceSelectRequest(QString id) +{ + setSelectedInstanceById(id); +} + +void MainWindow::instanceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + auto current = view->selectionModel()->currentIndex(); + QItemSelection test(topLeft, bottomRight); + if (test.contains(current)) + { + instanceChanged(current, current); + } +} + +void MainWindow::selectionBad() +{ + // start by reseting everything... + m_selectedInstance = nullptr; + + statusBar()->clearMessage(); + ui->instanceToolBar->setEnabled(false); + ui->renameButton->setText(tr("Rename Instance")); + updateInstanceToolIcon("grass"); + + // ...and then see if we can enable the previously selected instance + setSelectedInstanceById(APPLICATION->settings()->get("SelectedInstance").toString()); +} + +void MainWindow::checkInstancePathForProblems() +{ + QString instanceFolder = APPLICATION->settings()->get("InstanceDir").toString(); + if (FS::checkProblemticPathJava(QDir(instanceFolder))) + { + QMessageBox warning(this); + warning.setText(tr("Your instance folder contains \'!\' and this is known to cause Java problems!")); + warning.setInformativeText( + tr( + "You have now two options:
" + " - change the instance folder in the settings
" + " - move this installation of %1 to a different folder" + ).arg(BuildConfig.LAUNCHER_NAME) + ); + warning.setDefaultButton(QMessageBox::Ok); + warning.exec(); + } + auto tempFolderText = tr("This is a problem:
" + " - The launcher will likely be deleted without warning by the operating system
" + " - close the launcher now and extract it to a real location, not a temporary folder"); + QString pathfoldername = QDir(instanceFolder).absolutePath(); + if (pathfoldername.contains("Rar$", Qt::CaseInsensitive)) + { + QMessageBox warning(this); + warning.setText(tr("Your instance folder contains \'Rar$\' - that means you haven't extracted the launcher archive!")); + warning.setInformativeText(tempFolderText); + warning.setDefaultButton(QMessageBox::Ok); + warning.exec(); + } + else if (pathfoldername.startsWith(QDir::tempPath()) || pathfoldername.contains("/TempState/")) + { + QMessageBox warning(this); + warning.setText(tr("Your instance folder is in a temporary folder: \'%1\'!").arg(QDir::tempPath())); + warning.setInformativeText(tempFolderText); + warning.setDefaultButton(QMessageBox::Ok); + warning.exec(); + } +} + +void MainWindow::updateStatusCenter() +{ + m_statusCenter->setVisible(APPLICATION->settings()->get("ShowGlobalGameTime").toBool()); + + int timePlayed = APPLICATION->instances()->getTotalPlayTime(); + if (timePlayed > 0) { + if (APPLICATION->settings()->get("ShowGameTimeHours").toBool()) { + m_statusCenter->setText(tr("Total playtime: %1 hours").arg(Time::prettifyDurationHours(timePlayed))); + } else { + m_statusCenter->setText(tr("Total playtime: %1").arg(Time::prettifyDuration(timePlayed))); + } + } +} diff --git a/ultimmc/launcher/ui/MainWindow.h b/ultimmc/launcher/ui/MainWindow.h new file mode 100644 index 0000000..685adba --- /dev/null +++ b/ultimmc/launcher/ui/MainWindow.h @@ -0,0 +1,227 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include +#include + +#include "BaseInstance.h" +#include "minecraft/auth/MinecraftAccount.h" +#include "net/NetJob.h" +#include "updater/GoUpdate.h" + +class LaunchController; +class NewsChecker; +class NotificationChecker; +class QToolButton; +class InstanceProxyModel; +class LabeledToolButton; +class QLabel; +class MinecraftLauncher; +class BaseProfilerFactory; +class InstanceView; +class KonamiCode; +class InstanceTask; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + + class Ui; + +public: + explicit MainWindow(QWidget *parent = 0); + ~MainWindow(); + + bool eventFilter(QObject *obj, QEvent *ev) override; + void closeEvent(QCloseEvent *event) override; + void changeEvent(QEvent * event) override; + + void checkInstancePathForProblems(); + + void updatesAllowedChanged(bool allowed); + + void droppedURLs(QList urls); +signals: + void isClosing(); + +protected: + QMenu * createPopupMenu() override; + +private slots: + void onCatToggled(bool); + + void on_actionAbout_triggered(); + + void on_actionAddInstance_triggered(); + + void on_actionREDDIT_triggered(); + + void on_actionDISCORD_triggered(); + + void on_actionCopyInstance_triggered(); + + void on_actionChangeInstGroup_triggered(); + + void on_actionChangeInstIcon_triggered(); + void on_changeIconButton_clicked(bool) + { + on_actionChangeInstIcon_triggered(); + } + + void on_actionViewInstanceFolder_triggered(); + + void on_actionConfig_Folder_triggered(); + + void on_actionViewSelectedInstFolder_triggered(); + + void on_actionViewSelectedMCFolder_triggered(); + + void on_actionViewSelectedModsFolder_triggered(); + + void refreshInstances(); + + void on_actionViewCentralModsFolder_triggered(); + + void checkForUpdates(); + + void on_actionSettings_triggered(); + + void on_actionInstanceSettings_triggered(); + + void on_actionManageAccounts_triggered(); + + void on_actionReportBug_triggered(); + + void on_actionPatreon_triggered(); + + void on_actionMoreNews_triggered(); + + void newsButtonClicked(); + + void on_actionLaunchInstance_triggered(); + + void on_actionLaunchInstanceOffline_triggered(); + + void on_actionDeleteInstance_triggered(); + + void deleteGroup(); + + void on_actionExportInstance_triggered(); + + void on_actionRenameInstance_triggered(); + void on_renameButton_clicked(bool) + { + on_actionRenameInstance_triggered(); + } + + void on_actionEditInstance_triggered(); + + void on_actionEditInstNotes_triggered(); + + void on_actionMods_triggered(); + + void on_actionWorlds_triggered(); + + void on_actionScreenshots_triggered(); + + void on_actionCreateShortcut_triggered(); + + void taskEnd(); + + /** + * called when an icon is changed in the icon model. + */ + void iconUpdated(QString); + + void showInstanceContextMenu(const QPoint &); + + void updateToolsMenu(); + + void instanceActivated(QModelIndex); + + void instanceChanged(const QModelIndex ¤t, const QModelIndex &previous); + + void instanceSelectRequest(QString id); + + void instanceDataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + + void selectionBad(); + + void startTask(Task *task); + + void updateAvailable(GoUpdate::Status status); + + void updateNotAvailable(); + + void notificationsChanged(); + + void defaultAccountChanged(); + + void changeActiveAccount(); + + void repopulateAccountsMenu(); + + void updateNewsLabel(); + + /*! + * Runs the DownloadTask and installs updates. + */ + void downloadUpdates(GoUpdate::Status status); + + void konamiTriggered(); + + void globalSettingsClosed(); + +private: + void retranslateUi(); + + void addInstance(QString url = QString()); + void activateInstance(InstancePtr instance); + void setCatBackground(bool enabled); + void updateInstanceToolIcon(QString new_icon); + void setSelectedInstanceById(const QString &id); + void updateStatusCenter(); + + void runModalTask(Task *task); + void instanceFromInstanceTask(InstanceTask *task); + void finalizeInstance(InstancePtr inst); + +private: + std::unique_ptr ui; + + // these are managed by Qt's memory management model! + InstanceView *view = nullptr; + InstanceProxyModel *proxymodel = nullptr; + QToolButton *newsLabel = nullptr; + QLabel *m_statusLeft = nullptr; + QLabel *m_statusCenter = nullptr; + QMenu *accountMenu = nullptr; + QToolButton *accountMenuButton = nullptr; + KonamiCode * secretEventFilter = nullptr; + + unique_qobject_ptr m_newsChecker; + unique_qobject_ptr m_notificationChecker; + + InstancePtr m_selectedInstance; + QString m_currentInstIcon; + + // managed by the application object + Task *m_versionLoadTask = nullptr; +}; diff --git a/ultimmc/launcher/ui/dialogs/AboutDialog.cpp b/ultimmc/launcher/ui/dialogs/AboutDialog.cpp new file mode 100644 index 0000000..09c2235 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/AboutDialog.cpp @@ -0,0 +1,150 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AboutDialog.h" +#include "ui_AboutDialog.h" +#include +#include "Application.h" +#include "BuildConfig.h" + +#include + +#include "HoeDown.h" + +namespace { +// Credits +// This is a hack, but I can't think of a better way to do this easily without screwing with QTextDocument... +QString getCreditsHtml(QStringList patrons) +{ + QString output; + QTextStream stream(&output); + stream.setCodec(QTextCodec::codecForName("UTF-8")); + stream << "
\n"; + + stream << "

" << QObject::tr("Original Author", "About Credits") << "

\n"; + stream << "

Andrew Okin <forkk@forkk.net>

\n"; + + stream << "

" << QObject::tr("Maintainer", "About Credits") << "

\n"; + stream << "

Petr Mrázek <peterix@gmail.com>

\n"; + + // TODO: grab contributors from git history + /* + if(!contributors.isEmpty()) { + stream << "

" << QObject::tr("Contributors", "About Credits") << "

\n"; + for (auto &contributor : contributors) + { + stream << "

" << contributor << "

\n"; + } + } + */ + + if(!patrons.isEmpty()) { + stream << "

" << QObject::tr("Patrons", "About Credits") << "

\n"; + for (QString patron : patrons) + { + stream << "

" << patron << "

\n"; + } + } + + stream << "
\n"; + return output; +} + +QString getLicenseHtml() +{ + HoeDown hoedown; + QFile dataFile(":/documents/COPYING.md"); + dataFile.open(QIODevice::ReadOnly); + QString output = hoedown.process(dataFile.readAll()); + return output; +} + +} + +AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent), ui(new Ui::AboutDialog) +{ + ui->setupUi(this); + + QString launcherName = BuildConfig.LAUNCHER_NAME; + + setWindowTitle(tr("About %1").arg(launcherName)); + + QString chtml = getCreditsHtml(QStringList()); + ui->creditsText->setHtml(chtml); + + QString lhtml = getLicenseHtml(); + ui->licenseText->setHtml(lhtml); + + ui->urlLabel->setOpenExternalLinks(true); + + ui->icon->setPixmap(APPLICATION->getThemedIcon("logo").pixmap(64)); + ui->title->setText(launcherName); + + ui->versionLabel->setText(tr("Version") +": " + BuildConfig.printableVersionString()); + ui->platformLabel->setText(tr("Platform") +": " + BuildConfig.BUILD_PLATFORM); + + if (BuildConfig.VERSION_BUILD >= 0) + ui->buildNumLabel->setText(tr("Build Number") +": " + QString::number(BuildConfig.VERSION_BUILD)); + else + ui->buildNumLabel->setVisible(false); + + if (!BuildConfig.VERSION_CHANNEL.isEmpty()) + ui->channelLabel->setText(tr("Channel") +": " + BuildConfig.VERSION_CHANNEL); + else + ui->channelLabel->setVisible(false); + + ui->redistributionText->setHtml(tr( +"

We keep MultiMC open source because we think it's important to be able to see the source code for a project like this, and we do so using the Apache license.

\n" +"

Part of the reason for using the Apache license is we don't want people using the "MultiMC" name when redistributing the project. " +"This means people must take the time to go through the source code and remove all references to "MultiMC", including but not limited to the project " +"icon and the title of windows, (no MultiMC-fork in the title).

\n" +"

The Apache license covers reasonable use for the name - a mention of the project's origins in the About dialog and the license is acceptable. " +"However, it should be abundantly clear that the project is a fork without implying that you have our blessing.

" + )); + + QString urlText("

%1

"); + ui->urlLabel->setText(urlText.arg(BuildConfig.LAUNCHER_GIT)); + + QString copyText("© 2012-2021 %1"); + ui->copyLabel->setText(copyText.arg(BuildConfig.LAUNCHER_COPYRIGHT)); + + connect(ui->closeButton, SIGNAL(clicked()), SLOT(close())); + + connect(ui->aboutQt, &QPushButton::clicked, &QApplication::aboutQt); + + loadPatronList(); +} + +AboutDialog::~AboutDialog() +{ + delete ui; +} + +void AboutDialog::loadPatronList() +{ + netJob = new NetJob("Patreon Patron List", APPLICATION->network()); + netJob->addNetAction(Net::Download::makeByteArray(QUrl("https://files.multimc.org/patrons.txt"), &dataSink)); + connect(netJob.get(), &NetJob::succeeded, this, &AboutDialog::patronListLoaded); + netJob->start(); +} + +void AboutDialog::patronListLoaded() +{ + QString patronListStr(dataSink); + dataSink.clear(); + QString html = getCreditsHtml(patronListStr.split("\n", QString::SkipEmptyParts)); + ui->creditsText->setHtml(html); +} + diff --git a/ultimmc/launcher/ui/dialogs/AboutDialog.h b/ultimmc/launcher/ui/dialogs/AboutDialog.h new file mode 100644 index 0000000..cc4b885 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/AboutDialog.h @@ -0,0 +1,47 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace Ui +{ +class AboutDialog; +} + +class AboutDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AboutDialog(QWidget *parent = 0); + ~AboutDialog(); + +public +slots: + /// Starts loading a list of Patreon patrons. + void loadPatronList(); + + /// Slot for when the patron list loads successfully. + void patronListLoaded(); + +private: + Ui::AboutDialog *ui; + + NetJob::Ptr netJob; + QByteArray dataSink; +}; diff --git a/ultimmc/launcher/ui/dialogs/AboutDialog.ui b/ultimmc/launcher/ui/dialogs/AboutDialog.ui new file mode 100644 index 0000000..422e877 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/AboutDialog.ui @@ -0,0 +1,316 @@ + + + AboutDialog + + + + 0 + 0 + 783 + 843 + + + + + 450 + 400 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 64 + 64 + + + + + 64 + 64 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + 15 + + + + MultiMC 5 + + + Qt::AlignCenter + + + + + + + 0 + + + + About + + + + + + true + + + <html><head/><body><p>A custom launcher that makes managing Minecraft easier by allowing you to have multiple instances of Minecraft at once.</p></body></html> + + + Qt::AlignCenter + + + true + + + + + + + + 10 + + + + GIT URL + + + Qt::AlignCenter + + + + + + + + 8 + true + + + + COPYRIGHT + + + Qt::AlignCenter + + + + + + + Qt::Horizontal + + + + + + + Version: + + + Qt::AlignCenter + + + + + + + Platform: + + + Qt::AlignCenter + + + + + + + Build Number: + + + Qt::AlignCenter + + + + + + + Channel: + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 212 + + + + + + + + + Credits + + + + + + true + + + Qt::TextBrowserInteraction + + + + + + + + License + + + + + + + 0 + 0 + + + + + DejaVu Sans Mono + + + + true + + + Qt::TextBrowserInteraction + + + + + + + + Forking/Redistribution + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + + + + false + + + About Qt + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + + + + + + + tabWidget + creditsText + licenseText + redistributionText + aboutQt + closeButton + + + + diff --git a/ultimmc/launcher/ui/dialogs/CopyInstanceDialog.cpp b/ultimmc/launcher/ui/dialogs/CopyInstanceDialog.cpp new file mode 100644 index 0000000..e511398 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/CopyInstanceDialog.cpp @@ -0,0 +1,144 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "Application.h" +#include "CopyInstanceDialog.h" +#include "ui_CopyInstanceDialog.h" + +#include "ui/dialogs/IconPickerDialog.h" + +#include "BaseVersion.h" +#include "icons/IconList.h" +#include "tasks/Task.h" +#include "BaseInstance.h" +#include "InstanceList.h" + +CopyInstanceDialog::CopyInstanceDialog(InstancePtr original, QWidget *parent) + :QDialog(parent), ui(new Ui::CopyInstanceDialog), m_original(original) +{ + ui->setupUi(this); + resize(minimumSizeHint()); + layout()->setSizeConstraint(QLayout::SetFixedSize); + + InstIconKey = original->iconKey(); + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + ui->instNameTextBox->setText(original->name()); + ui->instNameTextBox->setFocus(); + auto groups = APPLICATION->instances()->getGroups().toSet(); + auto groupList = QStringList(groups.toList()); + groupList.sort(Qt::CaseInsensitive); + groupList.removeOne(""); + groupList.push_front(""); + ui->groupBox->addItems(groupList); + int index = groupList.indexOf(APPLICATION->instances()->getInstanceGroup(m_original->id())); + if(index == -1) + { + index = 0; + } + ui->groupBox->setCurrentIndex(index); + ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); + ui->copySavesCheckbox->setChecked(m_copySaves); + ui->keepPlaytimeCheckbox->setChecked(m_keepPlaytime); +} + +CopyInstanceDialog::~CopyInstanceDialog() +{ + delete ui; +} + +void CopyInstanceDialog::updateDialogState() +{ + auto allowOK = !instName().isEmpty(); + auto OkButton = ui->buttonBox->button(QDialogButtonBox::Ok); + if(OkButton->isEnabled() != allowOK) + { + OkButton->setEnabled(allowOK); + } +} + +QString CopyInstanceDialog::instName() const +{ + auto result = ui->instNameTextBox->text().trimmed(); + if(result.size()) + { + return result; + } + return QString(); +} + +QString CopyInstanceDialog::iconKey() const +{ + return InstIconKey; +} + +QString CopyInstanceDialog::instGroup() const +{ + return ui->groupBox->currentText(); +} + +void CopyInstanceDialog::on_iconButton_clicked() +{ + IconPickerDialog dlg(this); + dlg.execWithSelection(InstIconKey); + + if (dlg.result() == QDialog::Accepted) + { + InstIconKey = dlg.selectedIconKey; + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + } +} + +void CopyInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1) +{ + updateDialogState(); +} + +bool CopyInstanceDialog::shouldCopySaves() const +{ + return m_copySaves; +} + +void CopyInstanceDialog::on_copySavesCheckbox_stateChanged(int state) +{ + if(state == Qt::Unchecked) + { + m_copySaves = false; + } + else if(state == Qt::Checked) + { + m_copySaves = true; + } +} + +bool CopyInstanceDialog::shouldKeepPlaytime() const +{ + return m_keepPlaytime; +} + + +void CopyInstanceDialog::on_keepPlaytimeCheckbox_stateChanged(int state) +{ + if(state == Qt::Unchecked) + { + m_keepPlaytime = false; + } + else if(state == Qt::Checked) + { + m_keepPlaytime = true; + } +} diff --git a/ultimmc/launcher/ui/dialogs/CopyInstanceDialog.h b/ultimmc/launcher/ui/dialogs/CopyInstanceDialog.h new file mode 100644 index 0000000..bf3cd92 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/CopyInstanceDialog.h @@ -0,0 +1,58 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "BaseVersion.h" +#include + +class BaseInstance; + +namespace Ui +{ +class CopyInstanceDialog; +} + +class CopyInstanceDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CopyInstanceDialog(InstancePtr original, QWidget *parent = 0); + ~CopyInstanceDialog(); + + void updateDialogState(); + + QString instName() const; + QString instGroup() const; + QString iconKey() const; + bool shouldCopySaves() const; + bool shouldKeepPlaytime() const; + +private +slots: + void on_iconButton_clicked(); + void on_instNameTextBox_textChanged(const QString &arg1); + void on_copySavesCheckbox_stateChanged(int state); + void on_keepPlaytimeCheckbox_stateChanged(int state); + +private: + Ui::CopyInstanceDialog *ui; + QString InstIconKey; + InstancePtr m_original; + bool m_copySaves = true; + bool m_keepPlaytime = true; +}; diff --git a/ultimmc/launcher/ui/dialogs/CopyInstanceDialog.ui b/ultimmc/launcher/ui/dialogs/CopyInstanceDialog.ui new file mode 100644 index 0000000..f4b191e --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/CopyInstanceDialog.ui @@ -0,0 +1,182 @@ + + + CopyInstanceDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 345 + 323 + + + + Copy Instance + + + + :/icons/toolbar/copy:/icons/toolbar/copy + + + true + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + :/icons/instances/grass:/icons/instances/grass + + + + 80 + 80 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Name + + + + + + + Qt::Horizontal + + + + + + + + + &Group + + + groupBox + + + + + + + + 0 + 0 + + + + true + + + + + + + + + Copy saves + + + + + + + Keep play time + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + iconButton + instNameTextBox + groupBox + copySavesCheckbox + keepPlaytimeCheckbox + + + + + + + buttonBox + accepted() + CopyInstanceDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + CopyInstanceDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ultimmc/launcher/ui/dialogs/CreateShortcutDialog.cpp b/ultimmc/launcher/ui/dialogs/CreateShortcutDialog.cpp new file mode 100644 index 0000000..dc8415e --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/CreateShortcutDialog.cpp @@ -0,0 +1,262 @@ +/* + * Copyright 2022-2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include +#include +#include +#include "CreateShortcutDialog.h" +#include "ui_CreateShortcutDialog.h" +#include "Application.h" +#include "minecraft/auth/AccountList.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/WorldList.h" +#include "minecraft/VersionFilterData.h" +#include "minecraft/PackProfile.h" +#include "icons/IconList.h" + +#ifdef Q_OS_WIN +#include +#include +#endif + +CreateShortcutDialog::CreateShortcutDialog(QWidget *parent, InstancePtr instance) + :QDialog(parent), ui(new Ui::CreateShortcutDialog), m_instance(instance) +{ + ui->setupUi(this); + + QStringList accountNameList; + auto accounts = APPLICATION->accounts(); + + for (int i = 0; i < accounts->count(); i++) + { + accountNameList.append(accounts->at(i)->profileName()); + } + + ui->profileComboBox->addItems(accountNameList); + + if (accounts->defaultAccount()) + { + ui->profileComboBox->setCurrentText(accounts->defaultAccount()->profileName()); + } + + // TODO: check if version is affected by crashing when joining servers on launch, ideally in meta + + bool instanceSupportsQuickPlay = false; + + auto mcInstance = std::dynamic_pointer_cast(instance); + if (mcInstance) + { + mcInstance->getPackProfile()->reload(Net::Mode::Online); + + if (mcInstance->getPackProfile()->getComponent("net.minecraft")->getReleaseDateTime() >= g_VersionFilterData.quickPlayBeginsDate) + { + instanceSupportsQuickPlay = true; + mcInstance->worldList()->update(); + for (const auto &world : mcInstance->worldList()->allWorlds()) + { + ui->joinSingleplayer->addItem(world.folderName()); + } + } + } + + if (!instanceSupportsQuickPlay) + { + ui->joinServerRadioButton->setChecked(true); + ui->joinSingleplayerRadioButton->setVisible(false); + ui->joinSingleplayer->setVisible(false); + } + + // Macs don't have any concept of a desktop shortcut, so force-enable the option to generate a shell script instead +#if defined(Q_OS_UNIX) && !defined(Q_OS_LINUX) + ui->createScriptCheckBox->setEnabled(false); + ui->createScriptCheckBox->setChecked(true); +#endif + + connect(ui->joinWorldCheckBox, &QCheckBox::toggled, this, &CreateShortcutDialog::updateDialogState); + + updateDialogState(); +} + +CreateShortcutDialog::~CreateShortcutDialog() +{ + delete ui; +} + +void CreateShortcutDialog::on_shortcutPathBrowse_clicked() +{ + QString linkExtension; +#if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) + linkExtension = ui->createScriptCheckBox->isChecked() ? "sh" : "desktop"; +#endif +#ifdef Q_OS_MAC + linkExtension = "command"; +#endif +#ifdef Q_OS_WIN + linkExtension = ui->createScriptCheckBox->isChecked() ? "bat" : "lnk"; +#endif + QFileDialog fileDialog(this, tr("Select shortcut path"), QStandardPaths::writableLocation(QStandardPaths::DesktopLocation)); + fileDialog.setDefaultSuffix(linkExtension); + fileDialog.setAcceptMode(QFileDialog::AcceptSave); + fileDialog.setFileMode(QFileDialog::AnyFile); + fileDialog.selectFile(m_instance->name() + " - " + BuildConfig.LAUNCHER_DISPLAYNAME + "." + linkExtension); + if (fileDialog.exec()) + { + ui->shortcutPath->setText(fileDialog.selectedFiles().at(0)); + } + updateDialogState(); +} + +void CreateShortcutDialog::accept() +{ + createShortcut(); + QDialog::accept(); +} + + +void CreateShortcutDialog::updateDialogState() +{ + ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setEnabled( + !ui->shortcutPath->text().isEmpty() + && ( + !ui->joinWorldCheckBox->isChecked() + || (ui->joinServerRadioButton->isChecked() && !ui->joinServer->text().isEmpty()) + || (ui->joinSingleplayerRadioButton->isChecked() && !ui->joinSingleplayer->currentText().isEmpty()) + ) + && (!ui->offlineUsernameCheckBox->isChecked() || !ui->offlineUsername->text().isEmpty()) + && (!ui->useProfileCheckBox->isChecked() || !ui->profileComboBox->currentText().isEmpty()) + ); + ui->joinServer->setEnabled(ui->joinWorldCheckBox->isChecked() && ui->joinServerRadioButton->isChecked()); + ui->joinSingleplayer->setEnabled(ui->joinWorldCheckBox->isChecked() && ui->joinSingleplayerRadioButton->isChecked()); + ui->offlineUsername->setEnabled(ui->launchOfflineCheckBox->isChecked() && ui->offlineUsernameCheckBox->isChecked()); + if (!ui->launchOfflineCheckBox->isChecked()) + { + ui->offlineUsernameCheckBox->setChecked(false); + } +} + +QString CreateShortcutDialog::getLaunchCommand(bool escapeQuotesTwice) +{ + return "\"" + QDir::toNativeSeparators(QCoreApplication::applicationFilePath()).replace('"', escapeQuotesTwice ? "\\\\\"" : "\\\"") + "\"" + + getLaunchArgs(escapeQuotesTwice); +} + +QString CreateShortcutDialog::getLaunchArgs(bool escapeQuotesTwice) +{ + return " -d \"" + QDir::toNativeSeparators(QDir::currentPath()).replace('"', escapeQuotesTwice ? "\\\\\"" : "\\\"") + "\"" + + " -l \"" + m_instance->id() + "\"" + + (ui->joinServerRadioButton->isChecked() ? " -s \"" + ui->joinServer->text() + "\"" : "") + + (ui->joinSingleplayerRadioButton->isChecked() ? " -w \"" + ui->joinSingleplayer->currentText() + "\"" : "") + + (ui->useProfileCheckBox->isChecked() ? " -a \"" + ui->profileComboBox->currentText() + "\"" : "") + + (ui->launchOfflineCheckBox->isChecked() ? " -o" : "") + + (ui->offlineUsernameCheckBox->isChecked() ? " -n \"" + ui->offlineUsername->text() + "\"" : ""); +} + +void CreateShortcutDialog::createShortcut() +{ +#ifdef Q_OS_WIN + if (ui->createScriptCheckBox->isChecked()) // on windows, creating .lnk shortcuts requires specific win32 api stuff + // rather than just writing a text file + { +#endif + QString shortcutText; +#ifdef Q_OS_UNIX + // Unix shell script + if (ui->createScriptCheckBox->isChecked()) + { + shortcutText = "#!/bin/sh\n" + // FIXME: is there a way to use the launcher script instead of the raw binary here? + "cd \"" + QDir::currentPath().replace('"', "\\\"") + "\"\n" + + getLaunchCommand() + " &\n"; + } else + // freedesktop.org desktop entry + { + // save the launcher icon to a file so we can use it in the shortcut + if (!QFileInfo::exists(QDir::currentPath() + "/icons/shortcut-icon.png")) + { + QPixmap iconPixmap = QIcon(":/logo.svg").pixmap(64, 64); + iconPixmap.save(QDir::currentPath() + "/icons/shortcut-icon.png"); + } + + shortcutText = "[Desktop Entry]\n" + "Type=Application\n" + "Name=" + m_instance->name() + " - " + BuildConfig.LAUNCHER_DISPLAYNAME + "\n" + + "Exec=" + getLaunchCommand(true) + "\n" + + "Path=" + QDir::currentPath() + "\n" + + "Icon=" + QDir::currentPath() + "/icons/shortcut-icon.png\n"; + + } +#endif +#ifdef Q_OS_WIN + // Windows batch script implementation + shortcutText = "@ECHO OFF\r\n" + "CD \"" + QDir::toNativeSeparators(QDir::currentPath()) + "\"\r\n" + "START /B \"\" " + getLaunchCommand() + "\r\n"; +#endif + QFile shortcutFile(ui->shortcutPath->text()); + if (shortcutFile.open(QIODevice::WriteOnly)) + { + QTextStream stream(&shortcutFile); + stream << shortcutText; + shortcutFile.setPermissions(QFile::ReadOwner | QFile::ReadGroup | QFile::ReadOther + | QFile::WriteOwner | QFile::ExeOwner | QFile::ExeGroup); + shortcutFile.close(); + } +#ifdef Q_OS_WIN + } + else + { + if (!QFileInfo::exists(QDir::currentPath() + "/icons/shortcut-icon.ico")) + { + QPixmap iconPixmap = QIcon(":/logo.svg").pixmap(64, 64); + iconPixmap.save(QDir::currentPath() + "/icons/shortcut-icon.ico"); + } + + createWindowsLink(QDir::toNativeSeparators(QCoreApplication::applicationFilePath()).toStdString().c_str(), + QDir::toNativeSeparators(QDir::currentPath()).toStdString().c_str(), + getLaunchArgs().toStdString().c_str(), + ui->shortcutPath->text().toStdString().c_str(), + (m_instance->name() + " - " + BuildConfig.LAUNCHER_DISPLAYNAME).toStdString().c_str(), + QDir::toNativeSeparators(QDir::currentPath() + "/icons/shortcut-icon.ico").toStdString().c_str() + ); + } +#endif +} + +#ifdef Q_OS_WIN +void CreateShortcutDialog::createWindowsLink(LPCSTR target, LPCSTR workingDir, LPCSTR args, LPCSTR filename, + LPCSTR desc, LPCSTR iconPath) +{ + HRESULT result; + IShellLink *link; + + CoInitialize(nullptr); + result = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID *) &link); + if (SUCCEEDED(result)) + { + IPersistFile *file; + + link->SetPath(target); + link->SetWorkingDirectory(workingDir); + link->SetArguments(args); + link->SetDescription(desc); + link->SetIconLocation(iconPath, 0); + + result = link->QueryInterface(IID_IPersistFile, (LPVOID *) &file); + + if (SUCCEEDED(result)) + { + WCHAR path[MAX_PATH]; + MultiByteToWideChar(CP_ACP, 0, filename, -1, path, MAX_PATH); + + file->Save(path, TRUE); + file->Release(); + } + link->Release(); + } + CoUninitialize(); +} +#endif \ No newline at end of file diff --git a/ultimmc/launcher/ui/dialogs/CreateShortcutDialog.h b/ultimmc/launcher/ui/dialogs/CreateShortcutDialog.h new file mode 100644 index 0000000..4714253 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/CreateShortcutDialog.h @@ -0,0 +1,50 @@ +/* + * Copyright 2022 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include +#include "minecraft/auth/MinecraftAccount.h" +#include "BaseInstance.h" +#include "minecraft/ParseUtils.h" + +#ifdef Q_OS_WIN +#include +#endif + +namespace Ui +{ + class CreateShortcutDialog; +} + +class CreateShortcutDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CreateShortcutDialog(QWidget *parent = nullptr, InstancePtr instance = nullptr); + ~CreateShortcutDialog() override; + +private +slots: + void on_shortcutPathBrowse_clicked(); + void updateDialogState(); + void accept() override; + +private: + Ui::CreateShortcutDialog *ui; + InstancePtr m_instance; + + QString getLaunchCommand(bool escapeQuotesTwice = false); + QString getLaunchArgs(bool escapeQuotesTwice = false); + + void createShortcut(); + +#ifdef Q_OS_WIN + void createWindowsLink(LPCSTR target, LPCSTR workingDir, LPCSTR args, LPCSTR filename, LPCSTR desc, LPCSTR iconPath); +#endif +}; \ No newline at end of file diff --git a/ultimmc/launcher/ui/dialogs/CreateShortcutDialog.ui b/ultimmc/launcher/ui/dialogs/CreateShortcutDialog.ui new file mode 100644 index 0000000..5965965 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/CreateShortcutDialog.ui @@ -0,0 +1,461 @@ + + + + CreateShortcutDialog + + + + 0 + 0 + 796 + 330 + + + + + 796 + 230 + + + + Create Shortcut + + + + + + QLayout::SetDefaultConstraint + + + + + Shortcut path: + + + + + + + Use specific profile: + + + + + + + false + + + + + + + Launch in offline mode + + + + + + + false + + + + + + + Join world on launch: + + + + + + + false + + + + + + + false + + + + + + + + + + false + + + Set offline mode username: + + + + + + + false + + + Singleplayer world: + + + + + + + Browse + + + + + + + false + + + Server address: + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + 0 + + + + + Create script instead of shortcut + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + buttonBox + accepted() + CreateShortcutDialog + accept() + + + 397 + 207 + + + 397 + 114 + + + + + buttonBox + rejected() + CreateShortcutDialog + reject() + + + 397 + 207 + + + 397 + 114 + + + + + joinServer + textChanged(QString) + CreateShortcutDialog + updateDialogState() + + + 471 + 61 + + + 397 + 114 + + + + + joinWorldCheckBox + toggled(bool) + joinServerRadioButton + setEnabled(bool) + + + 122 + 61 + + + 140 + 93 + + + + + launchOfflineCheckBox + stateChanged(int) + CreateShortcutDialog + updateDialogState() + + + 122 + 130 + + + 397 + 114 + + + + + offlineUsername + textChanged(QString) + CreateShortcutDialog + updateDialogState() + + + 471 + 162 + + + 397 + 114 + + + + + offlineUsernameCheckBox + stateChanged(int) + CreateShortcutDialog + updateDialogState() + + + 122 + 162 + + + 397 + 114 + + + + + profileComboBox + currentTextChanged(QString) + CreateShortcutDialog + updateDialogState() + + + 471 + 98 + + + 397 + 114 + + + + + shortcutPath + textChanged(QString) + CreateShortcutDialog + updateDialogState() + + + 471 + 23 + + + 397 + 114 + + + + + useProfileCheckBox + stateChanged(int) + CreateShortcutDialog + updateDialogState() + + + 122 + 98 + + + 397 + 114 + + + + + joinWorldCheckBox + toggled(bool) + joinSingleplayerRadioButton + setEnabled(bool) + + + 140 + 59 + + + 140 + 132 + + + + + joinServerRadioButton + toggled(bool) + joinServer + setEnabled(bool) + + + 140 + 93 + + + 489 + 93 + + + + + joinSingleplayerRadioButton + toggled(bool) + joinSingleplayer + setEnabled(bool) + + + 140 + 132 + + + 489 + 132 + + + + + useProfileCheckBox + toggled(bool) + profileComboBox + setEnabled(bool) + + + 140 + 171 + + + 489 + 171 + + + + + launchOfflineCheckBox + toggled(bool) + offlineUsernameCheckBox + setEnabled(bool) + + + 140 + 205 + + + 140 + 239 + + + + + joinSingleplayer + currentTextChanged(QString) + CreateShortcutDialog + updateDialogState() + + + 489 + 132 + + + 397 + 164 + + + + + joinServerRadioButton + toggled(bool) + CreateShortcutDialog + updateDialogState() + + + 140 + 93 + + + 397 + 164 + + + + + joinSingleplayerRadioButton + toggled(bool) + CreateShortcutDialog + updateDialogState() + + + 140 + 132 + + + 397 + 164 + + + + + + updateDialogState() + + diff --git a/ultimmc/launcher/ui/dialogs/CustomMessageBox.cpp b/ultimmc/launcher/ui/dialogs/CustomMessageBox.cpp new file mode 100644 index 0000000..19029f6 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/CustomMessageBox.cpp @@ -0,0 +1,35 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "CustomMessageBox.h" + +namespace CustomMessageBox +{ +QMessageBox *selectable(QWidget *parent, const QString &title, const QString &text, + QMessageBox::Icon icon, QMessageBox::StandardButtons buttons, + QMessageBox::StandardButton defaultButton) +{ + QMessageBox *messageBox = new QMessageBox(parent); + messageBox->setWindowTitle(title); + messageBox->setText(text); + messageBox->setStandardButtons(buttons); + messageBox->setDefaultButton(defaultButton); + messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBox->setIcon(icon); + messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + + return messageBox; +} +} diff --git a/ultimmc/launcher/ui/dialogs/CustomMessageBox.h b/ultimmc/launcher/ui/dialogs/CustomMessageBox.h new file mode 100644 index 0000000..712c651 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/CustomMessageBox.h @@ -0,0 +1,26 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace CustomMessageBox +{ +QMessageBox *selectable(QWidget *parent, const QString &title, const QString &text, + QMessageBox::Icon icon = QMessageBox::NoIcon, + QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); +} diff --git a/ultimmc/launcher/ui/dialogs/EditAccountDialog.cpp b/ultimmc/launcher/ui/dialogs/EditAccountDialog.cpp new file mode 100644 index 0000000..002c064 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/EditAccountDialog.cpp @@ -0,0 +1,61 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "EditAccountDialog.h" +#include "ui_EditAccountDialog.h" +#include +#include + +EditAccountDialog::EditAccountDialog(const QString &text, QWidget *parent, int flags) + : QDialog(parent), ui(new Ui::EditAccountDialog) +{ + ui->setupUi(this); + + ui->label->setText(text); + ui->label->setVisible(!text.isEmpty()); + + ui->userTextBox->setEnabled(flags & UsernameField); + ui->passTextBox->setEnabled(flags & PasswordField); +} + +EditAccountDialog::~EditAccountDialog() +{ + delete ui; +} + +void EditAccountDialog::on_label_linkActivated(const QString &link) +{ + DesktopServices::openUrl(QUrl(link)); +} + +void EditAccountDialog::setUsername(const QString & user) const +{ + ui->userTextBox->setText(user); +} + +QString EditAccountDialog::username() const +{ + return ui->userTextBox->text(); +} + +void EditAccountDialog::setPassword(const QString & pass) const +{ + ui->passTextBox->setText(pass); +} + +QString EditAccountDialog::password() const +{ + return ui->passTextBox->text(); +} diff --git a/ultimmc/launcher/ui/dialogs/EditAccountDialog.h b/ultimmc/launcher/ui/dialogs/EditAccountDialog.h new file mode 100644 index 0000000..6b5eb4a --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/EditAccountDialog.h @@ -0,0 +1,56 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace Ui +{ +class EditAccountDialog; +} + +class EditAccountDialog : public QDialog +{ + Q_OBJECT + +public: + explicit EditAccountDialog(const QString &text = "", QWidget *parent = 0, + int flags = UsernameField | PasswordField); + ~EditAccountDialog(); + + void setUsername(const QString & user) const; + void setPassword(const QString & pass) const; + + QString username() const; + QString password() const; + + enum Flags + { + NoFlags = 0, + + //! Specifies that the dialog should have a username field. + UsernameField, + + //! Specifies that the dialog should have a password field. + PasswordField, + }; + +private slots: + void on_label_linkActivated(const QString &link); + +private: + Ui::EditAccountDialog *ui; +}; diff --git a/ultimmc/launcher/ui/dialogs/EditAccountDialog.ui b/ultimmc/launcher/ui/dialogs/EditAccountDialog.ui new file mode 100644 index 0000000..e87509b --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/EditAccountDialog.ui @@ -0,0 +1,94 @@ + + + EditAccountDialog + + + + 0 + 0 + 400 + 148 + + + + Login + + + + + + Message label placeholder. + + + Qt::RichText + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Email + + + + + + + QLineEdit::Password + + + Password + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + EditAccountDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + EditAccountDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ultimmc/launcher/ui/dialogs/ExportInstanceDialog.cpp b/ultimmc/launcher/ui/dialogs/ExportInstanceDialog.cpp new file mode 100644 index 0000000..1a16487 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ExportInstanceDialog.cpp @@ -0,0 +1,482 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ExportInstanceDialog.h" +#include "ui_ExportInstanceDialog.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include "MMCStrings.h" +#include "SeparatorPrefixTree.h" +#include "Application.h" +#include +#include + +class PackIgnoreProxy : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + PackIgnoreProxy(InstancePtr instance, QObject *parent) : QSortFilterProxyModel(parent) + { + m_instance = instance; + } + // NOTE: Sadly, we have to do sorting ourselves. + bool lessThan(const QModelIndex &left, const QModelIndex &right) const + { + QFileSystemModel *fsm = qobject_cast(sourceModel()); + if (!fsm) + { + return QSortFilterProxyModel::lessThan(left, right); + } + bool asc = sortOrder() == Qt::AscendingOrder ? true : false; + + QFileInfo leftFileInfo = fsm->fileInfo(left); + QFileInfo rightFileInfo = fsm->fileInfo(right); + + if (!leftFileInfo.isDir() && rightFileInfo.isDir()) + { + return !asc; + } + if (leftFileInfo.isDir() && !rightFileInfo.isDir()) + { + return asc; + } + + // sort and proxy model breaks the original model... + if (sortColumn() == 0) + { + return Strings::naturalCompare(leftFileInfo.fileName(), rightFileInfo.fileName(), + Qt::CaseInsensitive) < 0; + } + if (sortColumn() == 1) + { + auto leftSize = leftFileInfo.size(); + auto rightSize = rightFileInfo.size(); + if ((leftSize == rightSize) || (leftFileInfo.isDir() && rightFileInfo.isDir())) + { + return Strings::naturalCompare(leftFileInfo.fileName(), + rightFileInfo.fileName(), + Qt::CaseInsensitive) < 0 + ? asc + : !asc; + } + return leftSize < rightSize; + } + return QSortFilterProxyModel::lessThan(left, right); + } + + virtual Qt::ItemFlags flags(const QModelIndex &index) const + { + if (!index.isValid()) + return Qt::NoItemFlags; + + auto sourceIndex = mapToSource(index); + Qt::ItemFlags flags = sourceIndex.flags(); + if (index.column() == 0) + { + flags |= Qt::ItemIsUserCheckable; + if (sourceIndex.model()->hasChildren(sourceIndex)) + { + flags |= Qt::ItemIsTristate; + } + } + + return flags; + } + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const + { + QModelIndex sourceIndex = mapToSource(index); + + if (index.column() == 0 && role == Qt::CheckStateRole) + { + QFileSystemModel *fsm = qobject_cast(sourceModel()); + auto blockedPath = relPath(fsm->filePath(sourceIndex)); + auto cover = blocked.cover(blockedPath); + if (!cover.isNull()) + { + return QVariant(Qt::Unchecked); + } + else if (blocked.exists(blockedPath)) + { + return QVariant(Qt::PartiallyChecked); + } + else + { + return QVariant(Qt::Checked); + } + } + + return sourceIndex.data(role); + } + + virtual bool setData(const QModelIndex &index, const QVariant &value, + int role = Qt::EditRole) + { + if (index.column() == 0 && role == Qt::CheckStateRole) + { + Qt::CheckState state = static_cast(value.toInt()); + return setFilterState(index, state); + } + + QModelIndex sourceIndex = mapToSource(index); + return QSortFilterProxyModel::sourceModel()->setData(sourceIndex, value, role); + } + + QString relPath(const QString &path) const + { + QString prefix = QDir().absoluteFilePath(m_instance->instanceRoot()); + prefix += '/'; + if (!path.startsWith(prefix)) + { + return QString(); + } + return path.mid(prefix.size()); + } + + bool setFilterState(QModelIndex index, Qt::CheckState state) + { + QFileSystemModel *fsm = qobject_cast(sourceModel()); + + if (!fsm) + { + return false; + } + + QModelIndex sourceIndex = mapToSource(index); + auto blockedPath = relPath(fsm->filePath(sourceIndex)); + bool changed = false; + if (state == Qt::Unchecked) + { + // blocking a path + auto &node = blocked.insert(blockedPath); + // get rid of all blocked nodes below + node.clear(); + changed = true; + } + else if (state == Qt::Checked || state == Qt::PartiallyChecked) + { + if (!blocked.remove(blockedPath)) + { + auto cover = blocked.cover(blockedPath); + qDebug() << "Blocked by cover" << cover; + // uncover + blocked.remove(cover); + // block all contents, except for any cover + QModelIndex rootIndex = + fsm->index(FS::PathCombine(m_instance->instanceRoot(), cover)); + QModelIndex doing = rootIndex; + int row = 0; + QStack todo; + while (1) + { + auto node = doing.child(row, 0); + if (!node.isValid()) + { + if (!todo.size()) + { + break; + } + else + { + doing = todo.pop(); + row = 0; + continue; + } + } + auto relpath = relPath(fsm->filePath(node)); + if (blockedPath.startsWith(relpath)) // cover found? + { + // continue processing cover later + todo.push(node); + } + else + { + // or just block this one. + blocked.insert(relpath); + } + row++; + } + } + changed = true; + } + if (changed) + { + // update the thing + emit dataChanged(index, index, {Qt::CheckStateRole}); + // update everything above index + QModelIndex up = index.parent(); + while (1) + { + if (!up.isValid()) + break; + emit dataChanged(up, up, {Qt::CheckStateRole}); + up = up.parent(); + } + // and everything below the index + QModelIndex doing = index; + int row = 0; + QStack todo; + while (1) + { + auto node = doing.child(row, 0); + if (!node.isValid()) + { + if (!todo.size()) + { + break; + } + else + { + doing = todo.pop(); + row = 0; + continue; + } + } + emit dataChanged(node, node, {Qt::CheckStateRole}); + todo.push(node); + row++; + } + // siblings and unrelated nodes are ignored + } + return true; + } + + bool shouldExpand(QModelIndex index) + { + QModelIndex sourceIndex = mapToSource(index); + QFileSystemModel *fsm = qobject_cast(sourceModel()); + if (!fsm) + { + return false; + } + auto blockedPath = relPath(fsm->filePath(sourceIndex)); + auto found = blocked.find(blockedPath); + if(found) + { + return !found->leaf(); + } + return false; + } + + void setBlockedPaths(QStringList paths) + { + beginResetModel(); + blocked.clear(); + blocked.insert(paths); + endResetModel(); + } + + const SeparatorPrefixTree<'/'> & blockedPaths() const + { + return blocked; + } + +protected: + bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const + { + Q_UNUSED(source_parent) + + // adjust the columns you want to filter out here + // return false for those that will be hidden + if (source_column == 2 || source_column == 3) + return false; + + return true; + } + +private: + InstancePtr m_instance; + SeparatorPrefixTree<'/'> blocked; +}; + +ExportInstanceDialog::ExportInstanceDialog(InstancePtr instance, QWidget *parent) + : QDialog(parent), ui(new Ui::ExportInstanceDialog), m_instance(instance) +{ + ui->setupUi(this); + auto model = new QFileSystemModel(this); + proxyModel = new PackIgnoreProxy(m_instance, this); + loadPackIgnore(); + proxyModel->setSourceModel(model); + auto root = instance->instanceRoot(); + ui->treeView->setModel(proxyModel); + ui->treeView->setRootIndex(proxyModel->mapFromSource(model->index(root))); + ui->treeView->sortByColumn(0, Qt::AscendingOrder); + + connect(proxyModel, SIGNAL(rowsInserted(QModelIndex,int,int)), SLOT(rowsInserted(QModelIndex,int,int))); + + model->setFilter(QDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs | QDir::Hidden); + model->setRootPath(root); + auto headerView = ui->treeView->header(); + headerView->setSectionResizeMode(QHeaderView::ResizeToContents); + headerView->setSectionResizeMode(0, QHeaderView::Stretch); +} + +ExportInstanceDialog::~ExportInstanceDialog() +{ + delete ui; +} + +/// Save icon to instance's folder is needed +void SaveIcon(InstancePtr m_instance) +{ + auto iconKey = m_instance->iconKey(); + auto iconList = APPLICATION->icons(); + auto mmcIcon = iconList->icon(iconKey); + if(!mmcIcon || mmcIcon->isBuiltIn()) { + return; + } + auto path = mmcIcon->getFilePath(); + if(!path.isNull()) { + QFileInfo inInfo (path); + FS::copy(path, FS::PathCombine(m_instance->instanceRoot(), inInfo.fileName())) (); + return; + } + auto & image = mmcIcon->m_images[mmcIcon->type()]; + auto & icon = image.icon; + auto sizes = icon.availableSizes(); + if(sizes.size() == 0) + { + return; + } + auto areaOf = [](QSize size) + { + return size.width() * size.height(); + }; + QSize largest = sizes[0]; + // find variant with largest area + for(auto size: sizes) + { + if(areaOf(largest) < areaOf(size)) + { + largest = size; + } + } + auto pixmap = icon.pixmap(largest); + pixmap.save(FS::PathCombine(m_instance->instanceRoot(), iconKey + ".png")); +} + +bool ExportInstanceDialog::doExport() +{ + auto name = FS::RemoveInvalidFilenameChars(m_instance->name()); + + const QString output = QFileDialog::getSaveFileName( + this, tr("Export %1").arg(m_instance->name()), + FS::PathCombine(QDir::homePath(), name + ".zip"), "Zip (*.zip)", nullptr, QFileDialog::DontConfirmOverwrite); + if (output.isEmpty()) + { + return false; + } + if (QFile::exists(output)) + { + int ret = + QMessageBox::question(this, tr("Overwrite?"), + tr("This file already exists. Do you want to overwrite it?"), + QMessageBox::No, QMessageBox::Yes); + if (ret == QMessageBox::No) + { + return false; + } + } + + SaveIcon(m_instance); + + auto & blocked = proxyModel->blockedPaths(); + using std::placeholders::_1; + if (!JlCompress::compressDir(output, m_instance->instanceRoot(), name, std::bind(&SeparatorPrefixTree<'/'>::covers, blocked, _1))) + { + QMessageBox::warning(this, tr("Error"), tr("Unable to export instance")); + return false; + } + return true; +} + +void ExportInstanceDialog::done(int result) +{ + savePackIgnore(); + if (result == QDialog::Accepted) + { + if (doExport()) + { + QDialog::done(QDialog::Accepted); + return; + } + else + { + return; + } + } + QDialog::done(result); +} + +void ExportInstanceDialog::rowsInserted(QModelIndex parent, int top, int bottom) +{ + //WARNING: possible off-by-one? + for(int i = top; i < bottom; i++) + { + auto node = parent.child(i, 0); + if(proxyModel->shouldExpand(node)) + { + auto expNode = node.parent(); + if(!expNode.isValid()) + { + continue; + } + ui->treeView->expand(node); + } + } +} + +QString ExportInstanceDialog::ignoreFileName() +{ + return FS::PathCombine(m_instance->instanceRoot(), ".packignore"); +} + +void ExportInstanceDialog::loadPackIgnore() +{ + auto filename = ignoreFileName(); + QFile ignoreFile(filename); + if(!ignoreFile.open(QIODevice::ReadOnly)) + { + return; + } + auto data = ignoreFile.readAll(); + auto string = QString::fromUtf8(data); + proxyModel->setBlockedPaths(string.split('\n', QString::SkipEmptyParts)); +} + +void ExportInstanceDialog::savePackIgnore() +{ + auto data = proxyModel->blockedPaths().toStringList().join('\n').toUtf8(); + auto filename = ignoreFileName(); + try + { + FS::write(filename, data); + } + catch (const Exception &e) + { + qWarning() << e.cause(); + } +} + +#include "ExportInstanceDialog.moc" diff --git a/ultimmc/launcher/ui/dialogs/ExportInstanceDialog.h b/ultimmc/launcher/ui/dialogs/ExportInstanceDialog.h new file mode 100644 index 0000000..dea02d1 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ExportInstanceDialog.h @@ -0,0 +1,54 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +class BaseInstance; +class PackIgnoreProxy; +typedef std::shared_ptr InstancePtr; + +namespace Ui +{ +class ExportInstanceDialog; +} + +class ExportInstanceDialog : public QDialog +{ + Q_OBJECT + +public: + explicit ExportInstanceDialog(InstancePtr instance, QWidget *parent = 0); + ~ExportInstanceDialog(); + + virtual void done(int result); + +private: + bool doExport(); + void loadPackIgnore(); + void savePackIgnore(); + QString ignoreFileName(); + +private: + Ui::ExportInstanceDialog *ui; + InstancePtr m_instance; + PackIgnoreProxy * proxyModel; + +private slots: + void rowsInserted(QModelIndex parent, int top, int bottom); +}; diff --git a/ultimmc/launcher/ui/dialogs/ExportInstanceDialog.ui b/ultimmc/launcher/ui/dialogs/ExportInstanceDialog.ui new file mode 100644 index 0000000..bcd4e84 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ExportInstanceDialog.ui @@ -0,0 +1,83 @@ + + + ExportInstanceDialog + + + + 0 + 0 + 720 + 625 + + + + Export Instance + + + + + + true + + + QAbstractItemView::ExtendedSelection + + + true + + + false + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + treeView + + + + + buttonBox + accepted() + ExportInstanceDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + ExportInstanceDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ultimmc/launcher/ui/dialogs/IconPickerDialog.cpp b/ultimmc/launcher/ui/dialogs/IconPickerDialog.cpp new file mode 100644 index 0000000..fcb645d --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/IconPickerDialog.cpp @@ -0,0 +1,163 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "Application.h" + +#include "IconPickerDialog.h" +#include "ui_IconPickerDialog.h" + +#include "ui/instanceview/InstanceDelegate.h" + +#include "icons/IconList.h" +#include "icons/IconUtils.h" +#include + +IconPickerDialog::IconPickerDialog(QWidget *parent) + : QDialog(parent), ui(new Ui::IconPickerDialog) +{ + ui->setupUi(this); + setWindowModality(Qt::WindowModal); + + auto contentsWidget = ui->iconView; + contentsWidget->setViewMode(QListView::IconMode); + contentsWidget->setFlow(QListView::LeftToRight); + contentsWidget->setIconSize(QSize(48, 48)); + contentsWidget->setMovement(QListView::Static); + contentsWidget->setResizeMode(QListView::Adjust); + contentsWidget->setSelectionMode(QAbstractItemView::SingleSelection); + contentsWidget->setSpacing(5); + contentsWidget->setWordWrap(false); + contentsWidget->setWrapping(true); + contentsWidget->setUniformItemSizes(true); + contentsWidget->setTextElideMode(Qt::ElideRight); + contentsWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + contentsWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + contentsWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + contentsWidget->setItemDelegate(new ListViewDelegate()); + + // contentsWidget->setAcceptDrops(true); + contentsWidget->setDropIndicatorShown(true); + contentsWidget->viewport()->setAcceptDrops(true); + contentsWidget->setDragDropMode(QAbstractItemView::DropOnly); + contentsWidget->setDefaultDropAction(Qt::CopyAction); + + contentsWidget->installEventFilter(this); + + contentsWidget->setModel(APPLICATION->icons().get()); + + // NOTE: ResetRole forces the button to be on the left, while the OK/Cancel ones are on the right. We win. + auto buttonAdd = ui->buttonBox->addButton(tr("Add Icon"), QDialogButtonBox::ResetRole); + auto buttonRemove = ui->buttonBox->addButton(tr("Remove Icon"), QDialogButtonBox::ResetRole); + + connect(buttonAdd, SIGNAL(clicked(bool)), SLOT(addNewIcon())); + connect(buttonRemove, SIGNAL(clicked(bool)), SLOT(removeSelectedIcon())); + + connect(contentsWidget, SIGNAL(doubleClicked(QModelIndex)), SLOT(activated(QModelIndex))); + + connect(contentsWidget->selectionModel(), SIGNAL(selectionChanged(QItemSelection, QItemSelection)), SLOT(selectionChanged(QItemSelection, QItemSelection))); + + auto buttonFolder = ui->buttonBox->addButton(tr("Open Folder"), QDialogButtonBox::ResetRole); + connect(buttonFolder, &QPushButton::clicked, this, &IconPickerDialog::openFolder); +} + +bool IconPickerDialog::eventFilter(QObject *obj, QEvent *evt) +{ + if (obj != ui->iconView) + return QDialog::eventFilter(obj, evt); + if (evt->type() != QEvent::KeyPress) + { + return QDialog::eventFilter(obj, evt); + } + QKeyEvent *keyEvent = static_cast(evt); + switch (keyEvent->key()) + { + case Qt::Key_Delete: + removeSelectedIcon(); + return true; + case Qt::Key_Plus: + addNewIcon(); + return true; + default: + break; + } + return QDialog::eventFilter(obj, evt); +} + +void IconPickerDialog::addNewIcon() +{ + //: The title of the select icons open file dialog + QString selectIcons = tr("Select Icons"); + //: The type of icon files + auto filter = IconUtils::getIconFilter(); + QStringList fileNames = QFileDialog::getOpenFileNames(this, selectIcons, QString(), tr("Icons %1").arg(filter)); + APPLICATION->icons()->installIcons(fileNames); +} + +void IconPickerDialog::removeSelectedIcon() +{ + APPLICATION->icons()->deleteIcon(selectedIconKey); +} + +void IconPickerDialog::activated(QModelIndex index) +{ + selectedIconKey = index.data(Qt::UserRole).toString(); + accept(); +} + +void IconPickerDialog::selectionChanged(QItemSelection selected, QItemSelection deselected) +{ + if (selected.empty()) + return; + + QString key = selected.first().indexes().first().data(Qt::UserRole).toString(); + if (!key.isEmpty()) { + selectedIconKey = key; + } +} + +int IconPickerDialog::execWithSelection(QString selection) +{ + auto list = APPLICATION->icons(); + auto contentsWidget = ui->iconView; + selectedIconKey = selection; + + int index_nr = list->getIconIndex(selection); + auto model_index = list->index(index_nr); + contentsWidget->selectionModel()->select( + model_index, QItemSelectionModel::Current | QItemSelectionModel::Select); + + QMetaObject::invokeMethod(this, "delayed_scroll", Qt::QueuedConnection, Q_ARG(QModelIndex, model_index)); + return QDialog::exec(); +} + +void IconPickerDialog::delayed_scroll(QModelIndex model_index) +{ + auto contentsWidget = ui->iconView; + contentsWidget->scrollTo(model_index); +} + +IconPickerDialog::~IconPickerDialog() +{ + delete ui; +} + +void IconPickerDialog::openFolder() +{ + DesktopServices::openDirectory(APPLICATION->icons()->getDirectory(), true); +} diff --git a/ultimmc/launcher/ui/dialogs/IconPickerDialog.h b/ultimmc/launcher/ui/dialogs/IconPickerDialog.h new file mode 100644 index 0000000..9af6a67 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/IconPickerDialog.h @@ -0,0 +1,49 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include +#include + +namespace Ui +{ +class IconPickerDialog; +} + +class IconPickerDialog : public QDialog +{ + Q_OBJECT + +public: + explicit IconPickerDialog(QWidget *parent = 0); + ~IconPickerDialog(); + int execWithSelection(QString selection); + QString selectedIconKey; + +protected: + virtual bool eventFilter(QObject *, QEvent *); + +private: + Ui::IconPickerDialog *ui; + +private +slots: + void selectionChanged(QItemSelection, QItemSelection); + void activated(QModelIndex); + void delayed_scroll(QModelIndex); + void addNewIcon(); + void removeSelectedIcon(); + void openFolder(); +}; diff --git a/ultimmc/launcher/ui/dialogs/IconPickerDialog.ui b/ultimmc/launcher/ui/dialogs/IconPickerDialog.ui new file mode 100644 index 0000000..c548edf --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/IconPickerDialog.ui @@ -0,0 +1,67 @@ + + + IconPickerDialog + + + + 0 + 0 + 676 + 555 + + + + Pick icon + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + IconPickerDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + IconPickerDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ultimmc/launcher/ui/dialogs/LocalLoginDialog.cpp b/ultimmc/launcher/ui/dialogs/LocalLoginDialog.cpp new file mode 100644 index 0000000..8d37fdc --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/LocalLoginDialog.cpp @@ -0,0 +1,105 @@ +#include "LocalLoginDialog.h" +#include "ui_LocalLoginDialog.h" + +#include "minecraft/auth/AuthProviders.h" +#include "minecraft/auth/AccountTask.h" + +#include + +LocalLoginDialog::LocalLoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::LocalLoginDialog) +{ + ui->setupUi(this); + ui->progressBar->setVisible(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +LocalLoginDialog::~LocalLoginDialog() +{ + delete ui; +} + +// Stage 1: User interaction +void LocalLoginDialog::accept() +{ + setUserInputsEnabled(false); + ui->progressBar->setVisible(true); + + m_account = MinecraftAccount::createLocal(ui->userTextBox->text()); + m_account->setProvider(AuthProviders::lookup("local")); + + // Setup the login task and start it + m_loginTask = m_account->loginLocal(); + connect(m_loginTask.get(), &Task::failed, this, &LocalLoginDialog::onTaskFailed); + connect(m_loginTask.get(), &Task::succeeded, this, &LocalLoginDialog::onTaskSucceeded); + connect(m_loginTask.get(), &Task::status, this, &LocalLoginDialog::onTaskStatus); + connect(m_loginTask.get(), &Task::progress, this, &LocalLoginDialog::onTaskProgress); + if (!m_loginTask) + { + onTaskSucceeded(); + } else { + m_loginTask->start(); + } +} + +void LocalLoginDialog::setUserInputsEnabled(bool enable) +{ + ui->userTextBox->setEnabled(enable); + ui->buttonBox->setEnabled(enable); +} + +void LocalLoginDialog::on_userTextBox_textEdited(const QString &newText) +{ + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(!newText.isEmpty()); +} + +void LocalLoginDialog::onTaskFailed(const QString &reason) +{ + // Set message + auto lines = reason.split('\n'); + QString processed; + for(auto line: lines) { + if(line.size()) { + processed += "" + line + "
"; + } + else { + processed += "
"; + } + } + ui->label->setText(processed); + + // Re-enable user-interaction + setUserInputsEnabled(true); + ui->progressBar->setVisible(false); +} + +void LocalLoginDialog::onTaskSucceeded() +{ + QDialog::accept(); +} + +void LocalLoginDialog::onTaskStatus(const QString &status) +{ + ui->label->setText(status); +} + +void LocalLoginDialog::onTaskProgress(qint64 current, qint64 total) +{ + ui->progressBar->setMaximum(total); + ui->progressBar->setValue(current); +} + +// Public interface +MinecraftAccountPtr LocalLoginDialog::newAccount(QWidget *parent, QString msg) +{ + LocalLoginDialog dlg(parent); + dlg.ui->label->setText(msg); + if (dlg.exec() == QDialog::Accepted) + { + return dlg.m_account; + } + return 0; +} diff --git a/ultimmc/launcher/ui/dialogs/LocalLoginDialog.h b/ultimmc/launcher/ui/dialogs/LocalLoginDialog.h new file mode 100644 index 0000000..9f08c28 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/LocalLoginDialog.h @@ -0,0 +1,58 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "minecraft/auth/MinecraftAccount.h" +#include "tasks/Task.h" + +namespace Ui +{ +class LocalLoginDialog; +} + +class LocalLoginDialog : public QDialog +{ + Q_OBJECT + +public: + ~LocalLoginDialog(); + + static MinecraftAccountPtr newAccount(QWidget *parent, QString message); + +private: + explicit LocalLoginDialog(QWidget *parent = 0); + + void setUserInputsEnabled(bool enable); + +protected +slots: + void accept(); + + void onTaskFailed(const QString &reason); + void onTaskSucceeded(); + void onTaskStatus(const QString &status); + void onTaskProgress(qint64 current, qint64 total); + + void on_userTextBox_textEdited(const QString &newText); + +private: + Ui::LocalLoginDialog *ui; + MinecraftAccountPtr m_account; + Task::Ptr m_loginTask; +}; diff --git a/ultimmc/launcher/ui/dialogs/LocalLoginDialog.ui b/ultimmc/launcher/ui/dialogs/LocalLoginDialog.ui new file mode 100644 index 0000000..413615a --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/LocalLoginDialog.ui @@ -0,0 +1,70 @@ + + + LocalLoginDialog + + + + 0 + 0 + 421 + 100 + + + + + 0 + 0 + + + + Add Local Account + + + + + + Message label placeholder. + + + Qt::RichText + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Username + + + + + + + 24 + + + false + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/ultimmc/launcher/ui/dialogs/LoginDialog.cpp b/ultimmc/launcher/ui/dialogs/LoginDialog.cpp new file mode 100644 index 0000000..f72c58b --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/LoginDialog.cpp @@ -0,0 +1,127 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LoginDialog.h" +#include "ui_LoginDialog.h" + +#include "minecraft/auth/AuthProviders.h" +#include "minecraft/auth/AccountTask.h" + +#include + +LoginDialog::LoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::LoginDialog) +{ + ui->setupUi(this); + ui->progressBar->setVisible(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +LoginDialog::~LoginDialog() +{ + delete ui; +} + +// Stage 1: User interaction +void LoginDialog::accept() +{ + setUserInputsEnabled(false); + ui->progressBar->setVisible(true); + + m_account = MinecraftAccount::createElyby(ui->userTextBox->text()); + m_account->setProvider(AuthProviders::lookup("elyby")); + + // Setup the login task and start it + m_loginTask = m_account->loginElyby(ui->passTextBox->text()); + connect(m_loginTask.get(), &Task::failed, this, &LoginDialog::onTaskFailed); + connect(m_loginTask.get(), &Task::succeeded, this, &LoginDialog::onTaskSucceeded); + connect(m_loginTask.get(), &Task::status, this, &LoginDialog::onTaskStatus); + connect(m_loginTask.get(), &Task::progress, this, &LoginDialog::onTaskProgress); + if (!m_loginTask) + { + onTaskSucceeded(); + } else { + m_loginTask->start(); + } +} + +void LoginDialog::setUserInputsEnabled(bool enable) +{ + ui->userTextBox->setEnabled(enable); + ui->passTextBox->setEnabled(enable); + ui->buttonBox->setEnabled(enable); +} + +// Enable the OK button only when both textboxes contain something. +void LoginDialog::on_userTextBox_textEdited(const QString &newText) +{ + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty()); +} +void LoginDialog::on_passTextBox_textEdited(const QString &newText) +{ + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty()); +} + +void LoginDialog::onTaskFailed(const QString &reason) +{ + // Set message + auto lines = reason.split('\n'); + QString processed; + for(auto line: lines) { + if(line.size()) { + processed += "" + line + "
"; + } + else { + processed += "
"; + } + } + ui->label->setText(processed); + + // Re-enable user-interaction + setUserInputsEnabled(true); + ui->progressBar->setVisible(false); +} + +void LoginDialog::onTaskSucceeded() +{ + QDialog::accept(); +} + +void LoginDialog::onTaskStatus(const QString &status) +{ + ui->label->setText(status); +} + +void LoginDialog::onTaskProgress(qint64 current, qint64 total) +{ + ui->progressBar->setMaximum(total); + ui->progressBar->setValue(current); +} + +// Public interface +MinecraftAccountPtr LoginDialog::newAccount(QWidget *parent, QString msg) +{ + LoginDialog dlg(parent); + dlg.ui->label->setText(msg); + if (dlg.exec() == QDialog::Accepted) + { + return dlg.m_account; + } + return 0; +} diff --git a/ultimmc/launcher/ui/dialogs/LoginDialog.h b/ultimmc/launcher/ui/dialogs/LoginDialog.h new file mode 100644 index 0000000..f8101ff --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/LoginDialog.h @@ -0,0 +1,59 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "minecraft/auth/MinecraftAccount.h" +#include "tasks/Task.h" + +namespace Ui +{ +class LoginDialog; +} + +class LoginDialog : public QDialog +{ + Q_OBJECT + +public: + ~LoginDialog(); + + static MinecraftAccountPtr newAccount(QWidget *parent, QString message); + +private: + explicit LoginDialog(QWidget *parent = 0); + + void setUserInputsEnabled(bool enable); + +protected +slots: + void accept(); + + void onTaskFailed(const QString &reason); + void onTaskSucceeded(); + void onTaskStatus(const QString &status); + void onTaskProgress(qint64 current, qint64 total); + + void on_userTextBox_textEdited(const QString &newText); + void on_passTextBox_textEdited(const QString &newText); + +private: + Ui::LoginDialog *ui; + MinecraftAccountPtr m_account; + Task::Ptr m_loginTask; +}; diff --git a/ultimmc/launcher/ui/dialogs/LoginDialog.ui b/ultimmc/launcher/ui/dialogs/LoginDialog.ui new file mode 100644 index 0000000..8fa4a45 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/LoginDialog.ui @@ -0,0 +1,77 @@ + + + LoginDialog + + + + 0 + 0 + 421 + 198 + + + + + 0 + 0 + + + + Add Account + + + + + + Message label placeholder. + + + Qt::RichText + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Email + + + + + + + QLineEdit::Password + + + Password + + + + + + + 24 + + + false + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/ultimmc/launcher/ui/dialogs/MSALoginDialog.cpp b/ultimmc/launcher/ui/dialogs/MSALoginDialog.cpp new file mode 100644 index 0000000..41aac72 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/MSALoginDialog.cpp @@ -0,0 +1,153 @@ +/* Copyright 2013-2022 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MSALoginDialog.h" +#include "ui_MSALoginDialog.h" + +#include "minecraft/auth/AccountTask.h" + +#include +#include +#include + +MSALoginDialog::MSALoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::MSALoginDialog) +{ + ui->setupUi(this); + ui->progressBar->setVisible(false); + // ui->buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +int MSALoginDialog::exec() { + setUserInputsEnabled(false); + ui->progressBar->setVisible(true); + ui->copyCodeButton->setVisible(false); + + // Setup the login task and start it + m_account = MinecraftAccount::createBlankMSA(); + m_loginTask = m_account->loginMSA(); + connect(m_loginTask.get(), &Task::failed, this, &MSALoginDialog::onTaskFailed); + connect(m_loginTask.get(), &Task::succeeded, this, &MSALoginDialog::onTaskSucceeded); + connect(m_loginTask.get(), &Task::status, this, &MSALoginDialog::onTaskStatus); + connect(m_loginTask.get(), &Task::progress, this, &MSALoginDialog::onTaskProgress); + connect(m_loginTask.get(), &AccountTask::showVerificationUriAndCode, this, &MSALoginDialog::showVerificationUriAndCode); + connect(m_loginTask.get(), &AccountTask::hideVerificationUriAndCode, this, &MSALoginDialog::hideVerificationUriAndCode); + connect(&m_externalLoginTimer, &QTimer::timeout, this, &MSALoginDialog::externalLoginTick); + m_loginTask->start(); + + return QDialog::exec(); +} + + +MSALoginDialog::~MSALoginDialog() +{ + delete ui; +} + +void MSALoginDialog::externalLoginTick() { + m_externalLoginElapsed++; + ui->progressBar->setValue(m_externalLoginElapsed); + ui->progressBar->repaint(); + + if(m_externalLoginElapsed >= m_externalLoginTimeout) { + m_externalLoginTimer.stop(); + } +} + + +void MSALoginDialog::showVerificationUriAndCode(const QUrl& uri, const QString& code, int expiresIn) { + ui->copyCodeButton->setVisible(true); + + m_externalLoginElapsed = 0; + m_externalLoginTimeout = expiresIn; + + m_externalLoginTimer.setInterval(1000); + m_externalLoginTimer.setSingleShot(false); + m_externalLoginTimer.start(); + + ui->progressBar->setMaximum(expiresIn); + ui->progressBar->setValue(m_externalLoginElapsed); + + QString urlString = uri.toString(); + QString linkString = QString("%2").arg(urlString, urlString); + ui->label->setText(tr("

Please open up %1 in a browser and put in the code %2 to proceed with login.

").arg(linkString, code)); + + m_code = code; +} + +void MSALoginDialog::hideVerificationUriAndCode() { + ui->copyCodeButton->setVisible(false); + m_externalLoginTimer.stop(); +} + +void MSALoginDialog::setUserInputsEnabled(bool enable) +{ + ui->buttonBox->setEnabled(enable); +} + +void MSALoginDialog::onTaskFailed(const QString &reason) +{ + // Set message + auto lines = reason.split('\n'); + QString processed; + for(auto line: lines) { + if(line.size()) { + processed += "" + line + "
"; + } + else { + processed += "
"; + } + } + ui->label->setText(processed); + + // Re-enable user-interaction + setUserInputsEnabled(true); + ui->progressBar->setVisible(false); +} + +void MSALoginDialog::onTaskSucceeded() +{ + QDialog::accept(); +} + +void MSALoginDialog::onTaskStatus(const QString &status) +{ + ui->label->setText(status); +} + +void MSALoginDialog::onTaskProgress(qint64 current, qint64 total) +{ + ui->progressBar->setMaximum(total); + ui->progressBar->setValue(current); +} + +// Public interface +MinecraftAccountPtr MSALoginDialog::newAccount(QWidget *parent, QString msg) +{ + MSALoginDialog dlg(parent); + dlg.ui->label->setText(msg); + if (dlg.exec() == QDialog::Accepted) + { + return dlg.m_account; + } + return 0; +} + +void MSALoginDialog::on_copyCodeButton_clicked() +{ + QApplication::clipboard()->setText(m_code); +} \ No newline at end of file diff --git a/ultimmc/launcher/ui/dialogs/MSALoginDialog.h b/ultimmc/launcher/ui/dialogs/MSALoginDialog.h new file mode 100644 index 0000000..963f550 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/MSALoginDialog.h @@ -0,0 +1,65 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "minecraft/auth/MinecraftAccount.h" + +namespace Ui +{ +class MSALoginDialog; +} + +class MSALoginDialog : public QDialog +{ + Q_OBJECT + +public: + ~MSALoginDialog(); + + static MinecraftAccountPtr newAccount(QWidget *parent, QString message); + int exec() override; + +private: + explicit MSALoginDialog(QWidget *parent = 0); + + void setUserInputsEnabled(bool enable); + +protected +slots: + void onTaskFailed(const QString &reason); + void onTaskSucceeded(); + void onTaskStatus(const QString &status); + void onTaskProgress(qint64 current, qint64 total); + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + void hideVerificationUriAndCode(); + void on_copyCodeButton_clicked(); + + void externalLoginTick(); + +private: + Ui::MSALoginDialog *ui; + MinecraftAccountPtr m_account; + shared_qobject_ptr m_loginTask; + QTimer m_externalLoginTimer; + QString m_code; + int m_externalLoginElapsed = 0; + int m_externalLoginTimeout = 0; +}; + diff --git a/ultimmc/launcher/ui/dialogs/MSALoginDialog.ui b/ultimmc/launcher/ui/dialogs/MSALoginDialog.ui new file mode 100644 index 0000000..0921e38 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/MSALoginDialog.ui @@ -0,0 +1,76 @@ + + + MSALoginDialog + + + + 0 + 0 + 491 + 143 + + + + + 0 + 0 + + + + Add Microsoft Account + + + + + + Message label placeholder. + +aaaaa + + + Qt::RichText + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + 24 + + + false + + + + + + + + + Copy Code + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel + + + + + + + + + + diff --git a/ultimmc/launcher/ui/dialogs/ModrinthExportDialog.cpp b/ultimmc/launcher/ui/dialogs/ModrinthExportDialog.cpp new file mode 100644 index 0000000..24a66af --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ModrinthExportDialog.cpp @@ -0,0 +1,144 @@ +/* + * Copyright 2023-2024 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include +#include +#include +#include +#include "ModrinthExportDialog.h" +#include "ui_ModrinthExportDialog.h" +#include "BaseInstance.h" +#include "modplatform/modrinth/ModrinthInstanceExportTask.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "ProgressDialog.h" +#include "CustomMessageBox.h" + + +ModrinthExportDialog::ModrinthExportDialog(InstancePtr instance, QWidget *parent) : + QDialog(parent), ui(new Ui::ModrinthExportDialog), m_instance(instance) +{ + ui->setupUi(this); + ui->name->setText(m_instance->name()); + ui->version->setText("1.0"); +} + +void ModrinthExportDialog::updateDialogState() +{ + ui->buttonBox->button(QDialogButtonBox::StandardButton::Ok)->setEnabled( + !ui->name->text().isEmpty() + && !ui->version->text().isEmpty() + && ui->file->text().endsWith(".mrpack") + && ( + !ui->includeDatapacks->isChecked() + || (!ui->datapacksPath->text().isEmpty() && QDir(m_instance->gameRoot() + "/" + ui->datapacksPath->text()).exists()) + ) + ); +} + +void ModrinthExportDialog::on_fileBrowseButton_clicked() +{ + QFileDialog dialog(this, tr("Select modpack file"), QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); + dialog.setDefaultSuffix("mrpack"); + dialog.setNameFilter("Modrinth modpacks (*.mrpack)"); + dialog.setAcceptMode(QFileDialog::AcceptSave); + dialog.setFileMode(QFileDialog::AnyFile); + dialog.selectFile(ui->name->text() + ".mrpack"); + + if (dialog.exec()) { + ui->file->setText(dialog.selectedFiles().at(0)); + } + + updateDialogState(); +} + +void ModrinthExportDialog::on_datapackPathBrowse_clicked() +{ + QFileDialog dialog(this, tr("Select global datapacks folder"), m_instance->gameRoot()); + dialog.setAcceptMode(QFileDialog::AcceptOpen); + dialog.setFileMode(QFileDialog::DirectoryOnly); + + if (dialog.exec()) { + ui->datapacksPath->setText(QDir(m_instance->gameRoot()).relativeFilePath(dialog.selectedFiles().at(0))); + } + + updateDialogState(); +} + +void ModrinthExportDialog::accept() +{ + Modrinth::ExportSettings settings; + + settings.name = ui->name->text(); + settings.version = ui->version->text(); + settings.description = ui->description->text(); + + settings.includeGameConfig = ui->includeGameConfig->isChecked(); + settings.includeModConfigs = ui->includeModConfigs->isChecked(); + settings.includeResourcePacks = ui->includeResourcePacks->isChecked(); + settings.includeShaderPacks = ui->includeShaderPacks->isChecked(); + settings.treatDisabledAsOptional = ui->treatDisabledAsOptional->isChecked(); + + if (ui->includeDatapacks->isChecked()) { + settings.datapacksPath = ui->datapacksPath->text(); + } + + MinecraftInstancePtr minecraftInstance = std::dynamic_pointer_cast(m_instance); + minecraftInstance->getPackProfile()->reload(Net::Mode::Offline); + + for (int i = 0; i < minecraftInstance->getPackProfile()->rowCount(); i++) { + auto component = minecraftInstance->getPackProfile()->getComponent(i); + if (component->isCustom()) { + CustomMessageBox::selectable( + this, + tr("Warning"), + tr("Instance contains a custom component: %1\nThis cannot be exported to a Modrinth pack; the exported pack may not work correctly!") + .arg(component->getName()), + QMessageBox::Warning + )->exec(); + } + } + + settings.gameVersion = minecraftInstance->getPackProfile()->getComponentVersion("net.minecraft"); + settings.forgeVersion = minecraftInstance->getPackProfile()->getComponentVersion("net.minecraftforge"); + settings.fabricVersion = minecraftInstance->getPackProfile()->getComponentVersion("net.fabricmc.fabric-loader"); + settings.quiltVersion = minecraftInstance->getPackProfile()->getComponentVersion("org.quiltmc.quilt-loader"); + settings.neoforgeVersion = minecraftInstance->getPackProfile()->getComponentVersion("net.neoforged"); + + settings.exportPath = ui->file->text(); + + auto *task = new Modrinth::InstanceExportTask(m_instance, settings); + + connect(task, &Task::failed, [this](QString reason) + { + QString text; + if (reason.length() > 1000) { + text = reason.left(1000) + "..."; + } else { + text = reason; + } + CustomMessageBox::selectable(parentWidget(), tr("Error"), text, QMessageBox::Critical)->show(); + }); + connect(task, &Task::succeeded, [this, task]() + { + QStringList warnings = task->warnings(); + if(warnings.count()) + { + CustomMessageBox::selectable(parentWidget(), tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); + } + }); + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + loadDialog.execWithTask(task); + + QDialog::accept(); +} + +ModrinthExportDialog::~ModrinthExportDialog() +{ + delete ui; +} diff --git a/ultimmc/launcher/ui/dialogs/ModrinthExportDialog.h b/ultimmc/launcher/ui/dialogs/ModrinthExportDialog.h new file mode 100644 index 0000000..08a244c --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ModrinthExportDialog.h @@ -0,0 +1,38 @@ +/* + * Copyright 2023 arthomnix + * + * This source is subject to the Microsoft Public License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include +#include "ExportInstanceDialog.h" + +QT_BEGIN_NAMESPACE +namespace Ui +{ + class ModrinthExportDialog; +} +QT_END_NAMESPACE + +class ModrinthExportDialog : public QDialog +{ +Q_OBJECT + +public: + explicit ModrinthExportDialog(InstancePtr instance, QWidget *parent = nullptr); + + ~ModrinthExportDialog() override; + +private slots: + void on_fileBrowseButton_clicked(); + void on_datapackPathBrowse_clicked(); + void accept() override; + void updateDialogState(); + +private: + Ui::ModrinthExportDialog *ui; + InstancePtr m_instance; +}; \ No newline at end of file diff --git a/ultimmc/launcher/ui/dialogs/ModrinthExportDialog.ui b/ultimmc/launcher/ui/dialogs/ModrinthExportDialog.ui new file mode 100644 index 0000000..91c019c --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ModrinthExportDialog.ui @@ -0,0 +1,343 @@ + + + ModrinthExportDialog + + + + 0 + 0 + 835 + 559 + + + + + 0 + 0 + + + + Export Modrinth modpack + + + + + 10 + 10 + 821 + 541 + + + + + + + + 16777215 + 25 + + + + Export Modrinth modpack + + + + + + + + 16777215 + 200 + + + + Metadata + + + + + 10 + 30 + 801 + 151 + + + + + + + + + Name + + + + + + + + + + Version + + + + + + + Description + + + + + + + + + + + + + + + + + + + Export Options + + + + + 9 + 29 + 801 + 227 + + + + + + + QLayout::SetFixedSize + + + + + File + + + + + + + + + + Browse... + + + + + + + + + Include Minecraft config + + + true + + + + + + + Include mod configs + + + true + + + + + + + Include resource packs + + + + + + + Include shader packs + + + + + + + If enabled, all mods, shaders and resource packs that are available on Modrinth will be treated as optional on both client and server. Files that are unavailable on Modrinth will simply retain the ".disabled" extension as Modrinth packs don't support optional overrides. + + + Treat disabled mods, shaders and resource packs as optional + + + + + + + + + Use this if your modpack contains a mod which adds global datapacks. + + + Include global datapacks folder: + + + + + + + + + + Browse... + + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + buttonBox + accepted() + ModrinthExportDialog + accept() + + + 340 + 532 + + + 338 + 279 + + + + + buttonBox + rejected() + ModrinthExportDialog + reject() + + + 340 + 532 + + + 338 + 279 + + + + + name + textChanged(QString) + ModrinthExportDialog + updateDialogState() + + + 395 + 90 + + + 339 + 279 + + + + + version + textChanged(QString) + ModrinthExportDialog + updateDialogState() + + + 395 + 129 + + + 339 + 279 + + + + + file + textChanged(QString) + ModrinthExportDialog + updateDialogState() + + + 309 + 329 + + + 339 + 279 + + + + + datapacksPath + textChanged(QString) + ModrinthExportDialog + updateDialogState() + + + 532 + 472 + + + 417 + 279 + + + + + includeDatapacks + stateChanged(int) + ModrinthExportDialog + updateDialogState() + + + 183 + 472 + + + 417 + 279 + + + + + + updateDialogState() + + diff --git a/ultimmc/launcher/ui/dialogs/NewComponentDialog.cpp b/ultimmc/launcher/ui/dialogs/NewComponentDialog.cpp new file mode 100644 index 0000000..1bbafb0 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/NewComponentDialog.cpp @@ -0,0 +1,106 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Application.h" +#include "NewComponentDialog.h" +#include "ui_NewComponentDialog.h" + +#include +#include +#include +#include + +#include "VersionSelectDialog.h" +#include "ProgressDialog.h" +#include "IconPickerDialog.h" + +#include +#include +#include +#include + +#include +#include + +NewComponentDialog::NewComponentDialog(const QString & initialName, const QString & initialUid, QWidget *parent) + : QDialog(parent), ui(new Ui::NewComponentDialog) +{ + ui->setupUi(this); + resize(minimumSizeHint()); + + ui->nameTextBox->setText(initialName); + ui->uidTextBox->setText(initialUid); + + connect(ui->nameTextBox, &QLineEdit::textChanged, this, &NewComponentDialog::updateDialogState); + connect(ui->uidTextBox, &QLineEdit::textChanged, this, &NewComponentDialog::updateDialogState); + + auto groups = APPLICATION->instances()->getGroups().toSet(); + ui->nameTextBox->setFocus(); + + originalPlaceholderText = ui->uidTextBox->placeholderText(); + updateDialogState(); +} + +NewComponentDialog::~NewComponentDialog() +{ + delete ui; +} + +void NewComponentDialog::updateDialogState() +{ + auto protoUid = ui->nameTextBox->text().toLower(); + protoUid.remove(QRegularExpression("[^a-z]")); + if(protoUid.isEmpty()) + { + ui->uidTextBox->setPlaceholderText(originalPlaceholderText); + } + else + { + QString suggestedUid = "org.multimc.custom." + protoUid; + ui->uidTextBox->setPlaceholderText(suggestedUid); + } + bool allowOK = !name().isEmpty() && !uid().isEmpty() && !uidBlacklist.contains(uid()); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(allowOK); +} + +QString NewComponentDialog::name() const +{ + auto result = ui->nameTextBox->text(); + if(result.size()) + { + return result.trimmed(); + } + return QString(); +} + +QString NewComponentDialog::uid() const +{ + auto result = ui->uidTextBox->text(); + if(result.size()) + { + return result.trimmed(); + } + result = ui->uidTextBox->placeholderText(); + if(result.size() && result != originalPlaceholderText) + { + return result.trimmed(); + } + return QString(); +} + +void NewComponentDialog::setBlacklist(QStringList badUids) +{ + uidBlacklist = badUids; +} diff --git a/ultimmc/launcher/ui/dialogs/NewComponentDialog.h b/ultimmc/launcher/ui/dialogs/NewComponentDialog.h new file mode 100644 index 0000000..8c790be --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/NewComponentDialog.h @@ -0,0 +1,48 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include +#include + +namespace Ui +{ +class NewComponentDialog; +} + +class NewComponentDialog : public QDialog +{ + Q_OBJECT + +public: + explicit NewComponentDialog(const QString & initialName = QString(), const QString & initialUid = QString(), QWidget *parent = 0); + virtual ~NewComponentDialog(); + void setBlacklist(QStringList badUids); + + QString name() const; + QString uid() const; + +private slots: + void updateDialogState(); + +private: + Ui::NewComponentDialog *ui; + + QString originalPlaceholderText; + QStringList uidBlacklist; +}; diff --git a/ultimmc/launcher/ui/dialogs/NewComponentDialog.ui b/ultimmc/launcher/ui/dialogs/NewComponentDialog.ui new file mode 100644 index 0000000..03b0d22 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/NewComponentDialog.ui @@ -0,0 +1,101 @@ + + + NewComponentDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 345 + 146 + + + + Add Empty Component + + + + :/icons/toolbar/copy:/icons/toolbar/copy + + + true + + + + + + Name + + + + + + + uid + + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + nameTextBox + uidTextBox + + + + + + + buttonBox + accepted() + NewComponentDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + NewComponentDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/ultimmc/launcher/ui/dialogs/NewInstanceDialog.cpp b/ultimmc/launcher/ui/dialogs/NewInstanceDialog.cpp new file mode 100644 index 0000000..28be8fe --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/NewInstanceDialog.cpp @@ -0,0 +1,281 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Application.h" +#include "NewInstanceDialog.h" +#include "ui_NewInstanceDialog.h" + +#include +#include +#include +#include + +#include "VersionSelectDialog.h" +#include "ProgressDialog.h" +#include "IconPickerDialog.h" + +#include +#include +#include +#include +#include + +#include "ui/widgets/PageContainer.h" +#include "ui/pages/modplatform/VanillaPage.h" +#include "ui/pages/modplatform/atlauncher/AtlPage.h" +#include "ui/pages/modplatform/legacy_ftb/Page.h" +#include "ui/pages/modplatform/import_ftb/FTBAPage.h" +#include "ui/pages/modplatform/ImportPage.h" +#include "ui/pages/modplatform/modrinth/ModrinthPage.h" +#include "ui/pages/modplatform/technic/TechnicPage.h" + + + +NewInstanceDialog::NewInstanceDialog(const QString & initialGroup, const QString & url, QWidget *parent) + : QDialog(parent), ui(new Ui::NewInstanceDialog) +{ + ui->setupUi(this); + + setWindowIcon(APPLICATION->getThemedIcon("new")); + + InstIconKey = "default"; + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + + auto groups = APPLICATION->instances()->getGroups().toSet(); + auto groupList = QStringList(groups.toList()); + groupList.sort(Qt::CaseInsensitive); + groupList.removeOne(""); + groupList.push_front(initialGroup); + groupList.push_front(""); + ui->groupBox->addItems(groupList); + int index = groupList.indexOf(initialGroup); + if(index == -1) + { + index = 0; + } + ui->groupBox->setCurrentIndex(index); + ui->groupBox->lineEdit()->setPlaceholderText(tr("No group")); + + + // NOTE: m_buttons must be initialized before PageContainer, because it indirectly accesses m_buttons through setSuggestedPack! Do not move this below. + m_buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + + m_container = new PageContainer(this); + m_container->setSizePolicy(QSizePolicy::Policy::Preferred, QSizePolicy::Policy::Expanding); + m_container->layout()->setContentsMargins(0, 0, 0, 0); + ui->verticalLayout->insertWidget(2, m_container); + + m_container->addButtons(m_buttons); + + // Bonk Qt over its stupid head and make sure it understands which button is the default one... + // See: https://stackoverflow.com/questions/24556831/qbuttonbox-set-default-button + auto OkButton = m_buttons->button(QDialogButtonBox::Ok); + OkButton->setDefault(true); + OkButton->setAutoDefault(true); + connect(OkButton, &QPushButton::clicked, this, &NewInstanceDialog::accept); + + auto CancelButton = m_buttons->button(QDialogButtonBox::Cancel); + CancelButton->setDefault(false); + CancelButton->setAutoDefault(false); + connect(CancelButton, &QPushButton::clicked, this, &NewInstanceDialog::reject); + + auto HelpButton = m_buttons->button(QDialogButtonBox::Help); + HelpButton->setDefault(false); + HelpButton->setAutoDefault(false); + connect(HelpButton, &QPushButton::clicked, m_container, &PageContainer::help); + + if(!url.isEmpty()) + { + QUrl actualUrl(url); + m_container->selectPage("import"); + importPage->setUrl(url); + } + + connect(APPLICATION, &QApplication::focusChanged, this, &NewInstanceDialog::onFocusChanged); + + updateDialogState(); + + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("NewInstanceGeometry").toByteArray())); +} + +void NewInstanceDialog::reject() +{ + APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); + QDialog::reject(); +} + +void NewInstanceDialog::accept() +{ + APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); + importIconNow(); + QDialog::accept(); +} + +QList NewInstanceDialog::getPages() +{ + importPage = new ImportPage(this); + auto technicPage = new TechnicPage(this); + return + { + new VanillaPage(this), + importPage, + new ModrinthPage(this), + new AtlPage(this), + new ImportFTB::FTBAPage(this), + new LegacyFTB::Page(this), + technicPage + }; +} + +QString NewInstanceDialog::dialogTitle() +{ + return tr("New Instance"); +} + +NewInstanceDialog::~NewInstanceDialog() +{ + delete ui; +} + +void NewInstanceDialog::setSuggestedPack(const QString& name, InstanceTask* task) +{ + creationTask.reset(task); + + defaultInstName = name; + ui->instNameTextBox->setPlaceholderText(name); + + if (!instNameChanged) + { + ui->instNameTextBox->setText(name); + } + + if(!task) + { + ui->iconButton->setIcon(APPLICATION->icons()->getIcon("default")); + importIcon = false; + } + + auto allowOK = task && !instName().isEmpty(); + m_buttons->button(QDialogButtonBox::Ok)->setEnabled(allowOK); +} + +void NewInstanceDialog::setSuggestedIconFromFile(const QString &path, const QString &name) +{ + importIcon = true; + importIconPath = path; + importIconName = name; + + //Hmm, for some reason they can be to small + ui->iconButton->setIcon(QIcon(path)); +} + +void NewInstanceDialog::setSuggestedIcon(const QString &key) +{ + auto icon = APPLICATION->icons()->getIcon(key); + importIcon = false; + + ui->iconButton->setIcon(icon); +} + +InstanceTask * NewInstanceDialog::extractTask() +{ + InstanceTask * extracted = creationTask.get(); + creationTask.release(); + extracted->setName(instName()); + extracted->setGroup(instGroup()); + extracted->setIcon(iconKey()); + return extracted; +} + +void NewInstanceDialog::updateDialogState() +{ + auto allowOK = creationTask && !instName().isEmpty(); + auto OkButton = m_buttons->button(QDialogButtonBox::Ok); + if(OkButton->isEnabled() != allowOK) + { + OkButton->setEnabled(allowOK); + } +} + +QString NewInstanceDialog::instName() const +{ + auto result = ui->instNameTextBox->text().trimmed(); + if(result.size()) + { + return result; + } + result = ui->instNameTextBox->placeholderText().trimmed(); + if(result.size()) + { + return result; + } + return QString(); +} + +QString NewInstanceDialog::instGroup() const +{ + return ui->groupBox->currentText(); +} +QString NewInstanceDialog::iconKey() const +{ + return InstIconKey; +} + +void NewInstanceDialog::on_iconButton_clicked() +{ + importIconNow(); //so the user can switch back + IconPickerDialog dlg(this); + dlg.execWithSelection(InstIconKey); + + if (dlg.result() == QDialog::Accepted) + { + InstIconKey = dlg.selectedIconKey; + ui->iconButton->setIcon(APPLICATION->icons()->getIcon(InstIconKey)); + importIcon = false; + } +} + +void NewInstanceDialog::on_resetNameButton_clicked() +{ + ui->instNameTextBox->setText(defaultInstName); + instNameChanged = false; +} + +void NewInstanceDialog::on_instNameTextBox_textChanged(const QString &arg1) +{ + updateDialogState(); +} + +void NewInstanceDialog::on_instNameTextBox_textEdited(const QString &text) +{ + instNameChanged = true; +} + +void NewInstanceDialog::onFocusChanged(QWidget *, QWidget *newWidget) +{ + if (newWidget == ui->instNameTextBox && !instNameChanged) { + QTimer::singleShot(0, ui->instNameTextBox, &QLineEdit::selectAll); + } +} + +void NewInstanceDialog::importIconNow() +{ + if(importIcon) { + APPLICATION->icons()->installIcon(importIconPath, importIconName); + InstIconKey = importIconName; + importIcon = false; + } + APPLICATION->settings()->set("NewInstanceGeometry", saveGeometry().toBase64()); +} diff --git a/ultimmc/launcher/ui/dialogs/NewInstanceDialog.h b/ultimmc/launcher/ui/dialogs/NewInstanceDialog.h new file mode 100644 index 0000000..a001ace --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/NewInstanceDialog.h @@ -0,0 +1,84 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "BaseVersion.h" +#include "ui/pages/BasePageProvider.h" +#include "InstanceTask.h" + +namespace Ui +{ +class NewInstanceDialog; +} + +class PageContainer; +class QDialogButtonBox; +class ImportPage; + +class NewInstanceDialog : public QDialog, public BasePageProvider +{ + Q_OBJECT + +public: + explicit NewInstanceDialog(const QString & initialGroup, const QString & url = QString(), QWidget *parent = 0); + ~NewInstanceDialog(); + + void updateDialogState(); + + void setSuggestedPack(const QString & name = QString(), InstanceTask * task = nullptr); + void setSuggestedIconFromFile(const QString &path, const QString &name); + void setSuggestedIcon(const QString &key); + + InstanceTask * extractTask(); + + QString dialogTitle() override; + QList getPages() override; + + QString instName() const; + QString instGroup() const; + QString iconKey() const; + +public slots: + void accept() override; + void reject() override; + void onFocusChanged(QWidget *, QWidget *newWidget); + +private slots: + void on_iconButton_clicked(); + void on_resetNameButton_clicked(); + void on_instNameTextBox_textChanged(const QString &arg1); + void on_instNameTextBox_textEdited(const QString &text); + +private: + Ui::NewInstanceDialog *ui = nullptr; + PageContainer * m_container = nullptr; + QDialogButtonBox * m_buttons = nullptr; + + QString InstIconKey; + ImportPage *importPage = nullptr; + std::unique_ptr creationTask; + + bool importIcon = false; + QString importIconPath; + QString importIconName; + + void importIconNow(); + + QString defaultInstName; + bool instNameChanged = false; +}; diff --git a/ultimmc/launcher/ui/dialogs/NewInstanceDialog.ui b/ultimmc/launcher/ui/dialogs/NewInstanceDialog.ui new file mode 100644 index 0000000..90d6bf3 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/NewInstanceDialog.ui @@ -0,0 +1,94 @@ + + + NewInstanceDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 730 + 127 + + + + New Instance + + + + :/icons/toolbar/new:/icons/toolbar/new + + + true + + + + + + + + + + + true + + + + + + + &Name: + + + instNameTextBox + + + + + + + &Group: + + + groupBox + + + + + + + + 80 + 80 + + + + + + + + Reset + + + + + + + + + Qt::Horizontal + + + + + + + iconButton + instNameTextBox + groupBox + + + + diff --git a/ultimmc/launcher/ui/dialogs/NotificationDialog.cpp b/ultimmc/launcher/ui/dialogs/NotificationDialog.cpp new file mode 100644 index 0000000..f2a35ae --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/NotificationDialog.cpp @@ -0,0 +1,86 @@ +#include "NotificationDialog.h" +#include "ui_NotificationDialog.h" + +#include +#include + +NotificationDialog::NotificationDialog(const NotificationChecker::NotificationEntry &entry, QWidget *parent) : + QDialog(parent, Qt::MSWindowsFixedSizeDialogHint | Qt::WindowTitleHint | Qt::CustomizeWindowHint), + ui(new Ui::NotificationDialog) +{ + ui->setupUi(this); + + QStyle::StandardPixmap icon; + switch (entry.type) + { + case NotificationChecker::NotificationEntry::Critical: + icon = QStyle::SP_MessageBoxCritical; + break; + case NotificationChecker::NotificationEntry::Warning: + icon = QStyle::SP_MessageBoxWarning; + break; + default: + case NotificationChecker::NotificationEntry::Information: + icon = QStyle::SP_MessageBoxInformation; + break; + } + ui->iconLabel->setPixmap(style()->standardPixmap(icon, 0, this)); + ui->messageLabel->setText(entry.message); + + m_dontShowAgainText = tr("Don't show again"); + m_closeText = tr("Close"); + + ui->dontShowAgainBtn->setText(m_dontShowAgainText + QString(" (%1)").arg(m_dontShowAgainTime)); + ui->closeBtn->setText(m_closeText + QString(" (%1)").arg(m_closeTime)); + + startTimer(1000); +} + +NotificationDialog::~NotificationDialog() +{ + delete ui; +} + +void NotificationDialog::timerEvent(QTimerEvent *event) +{ + if (m_dontShowAgainTime > 0) + { + m_dontShowAgainTime--; + if (m_dontShowAgainTime == 0) + { + ui->dontShowAgainBtn->setText(m_dontShowAgainText); + ui->dontShowAgainBtn->setEnabled(true); + } + else + { + ui->dontShowAgainBtn->setText(m_dontShowAgainText + QString(" (%1)").arg(m_dontShowAgainTime)); + } + } + if (m_closeTime > 0) + { + m_closeTime--; + if (m_closeTime == 0) + { + ui->closeBtn->setText(m_closeText); + ui->closeBtn->setEnabled(true); + } + else + { + ui->closeBtn->setText(m_closeText + QString(" (%1)").arg(m_closeTime)); + } + } + + if (m_closeTime == 0 && m_dontShowAgainTime == 0) + { + killTimer(event->timerId()); + } +} + +void NotificationDialog::on_dontShowAgainBtn_clicked() +{ + done(DontShowAgain); +} +void NotificationDialog::on_closeBtn_clicked() +{ + done(Normal); +} diff --git a/ultimmc/launcher/ui/dialogs/NotificationDialog.h b/ultimmc/launcher/ui/dialogs/NotificationDialog.h new file mode 100644 index 0000000..e1cbb9f --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/NotificationDialog.h @@ -0,0 +1,44 @@ +#ifndef NOTIFICATIONDIALOG_H +#define NOTIFICATIONDIALOG_H + +#include + +#include "notifications/NotificationChecker.h" + +namespace Ui { +class NotificationDialog; +} + +class NotificationDialog : public QDialog +{ + Q_OBJECT + +public: + explicit NotificationDialog(const NotificationChecker::NotificationEntry &entry, QWidget *parent = 0); + ~NotificationDialog(); + + enum ExitCode + { + Normal, + DontShowAgain + }; + +protected: + void timerEvent(QTimerEvent *event); + +private: + Ui::NotificationDialog *ui; + + int m_dontShowAgainTime = 10; + int m_closeTime = 5; + + QString m_dontShowAgainText; + QString m_closeText; + +private +slots: + void on_dontShowAgainBtn_clicked(); + void on_closeBtn_clicked(); +}; + +#endif // NOTIFICATIONDIALOG_H diff --git a/ultimmc/launcher/ui/dialogs/NotificationDialog.ui b/ultimmc/launcher/ui/dialogs/NotificationDialog.ui new file mode 100644 index 0000000..3e6c22b --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/NotificationDialog.ui @@ -0,0 +1,85 @@ + + + NotificationDialog + + + + 0 + 0 + 320 + 240 + + + + Notification + + + + + + + + TextLabel + + + + + + + TextLabel + + + true + + + true + + + Qt::TextBrowserInteraction + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + Don't show again + + + + + + + false + + + Close + + + + + + + + + + diff --git a/ultimmc/launcher/ui/dialogs/ProfileSelectDialog.cpp b/ultimmc/launcher/ui/dialogs/ProfileSelectDialog.cpp new file mode 100644 index 0000000..7882cf4 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ProfileSelectDialog.cpp @@ -0,0 +1,115 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ProfileSelectDialog.h" +#include "ui_ProfileSelectDialog.h" + +#include +#include + +#include "SkinUtils.h" +#include "Application.h" + +#include "ui/dialogs/ProgressDialog.h" + +ProfileSelectDialog::ProfileSelectDialog(const QString &message, int flags, QWidget *parent) + : QDialog(parent), ui(new Ui::ProfileSelectDialog) +{ + ui->setupUi(this); + + m_accounts = APPLICATION->accounts(); + auto view = ui->listView; + //view->setModel(m_accounts.get()); + //view->hideColumn(AccountList::ActiveColumn); + view->setColumnCount(1); + view->setRootIsDecorated(false); + // FIXME: use a real model, not this + if(QTreeWidgetItem* header = view->headerItem()) + { + header->setText(0, tr("Name")); + } + else + { + view->setHeaderLabel(tr("Name")); + } + QList items; + for (int i = 0; i < m_accounts->count(); i++) + { + MinecraftAccountPtr account = m_accounts->at(i); + QString profileLabel; + if(account->isInUse()) { + profileLabel = tr("%1 (in use)").arg(account->profileName()); + } + else { + profileLabel = account->profileName(); + } + auto item = new QTreeWidgetItem(view); + item->setText(0, profileLabel); + item->setIcon(0, account->getFace()); + item->setData(0, AccountList::PointerRole, QVariant::fromValue(account)); + items.append(item); + } + view->addTopLevelItems(items); + + // Set the message label. + ui->msgLabel->setVisible(!message.isEmpty()); + ui->msgLabel->setText(message); + + // Flags... + ui->globalDefaultCheck->setVisible(flags & GlobalDefaultCheckbox); + ui->instDefaultCheck->setVisible(flags & InstanceDefaultCheckbox); + qDebug() << flags; + + // Select the first entry in the list. + ui->listView->setCurrentIndex(ui->listView->model()->index(0, 0)); + + connect(ui->listView, SIGNAL(doubleClicked(QModelIndex)), SLOT(on_buttonBox_accepted())); +} + +ProfileSelectDialog::~ProfileSelectDialog() +{ + delete ui; +} + +MinecraftAccountPtr ProfileSelectDialog::selectedAccount() const +{ + return m_selected; +} + +bool ProfileSelectDialog::useAsGlobalDefault() const +{ + return ui->globalDefaultCheck->isChecked(); +} + +bool ProfileSelectDialog::useAsInstDefaullt() const +{ + return ui->instDefaultCheck->isChecked(); +} + +void ProfileSelectDialog::on_buttonBox_accepted() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) + { + QModelIndex selected = selection.first(); + m_selected = selected.data(AccountList::PointerRole).value(); + } + close(); +} + +void ProfileSelectDialog::on_buttonBox_rejected() +{ + close(); +} diff --git a/ultimmc/launcher/ui/dialogs/ProfileSelectDialog.h b/ultimmc/launcher/ui/dialogs/ProfileSelectDialog.h new file mode 100644 index 0000000..38aa424 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ProfileSelectDialog.h @@ -0,0 +1,90 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include + +#include "minecraft/auth/AccountList.h" + +namespace Ui +{ +class ProfileSelectDialog; +} + +class ProfileSelectDialog : public QDialog +{ + Q_OBJECT +public: + enum Flags + { + NoFlags = 0, + + /*! + * Shows a check box on the dialog that allows the user to specify that the account + * they've selected should be used as the global default for all instances. + */ + GlobalDefaultCheckbox, + + /*! + * Shows a check box on the dialog that allows the user to specify that the account + * they've selected should be used as the default for the instance they are currently launching. + * This is not currently implemented. + */ + InstanceDefaultCheckbox, + }; + + /*! + * Constructs a new account select dialog with the given parent and message. + * The message will be shown at the top of the dialog. It is an empty string by default. + */ + explicit ProfileSelectDialog(const QString& message="", int flags=0, QWidget *parent = 0); + ~ProfileSelectDialog(); + + /*! + * Gets a pointer to the account that the user selected. + * This is null if the user clicked cancel or hasn't clicked OK yet. + */ + MinecraftAccountPtr selectedAccount() const; + + /*! + * Returns true if the user checked the "use as global default" checkbox. + * If the checkbox wasn't shown, this function returns false. + */ + bool useAsGlobalDefault() const; + + /*! + * Returns true if the user checked the "use as instance default" checkbox. + * If the checkbox wasn't shown, this function returns false. + */ + bool useAsInstDefaullt() const; + +public +slots: + void on_buttonBox_accepted(); + + void on_buttonBox_rejected(); + +protected: + shared_qobject_ptr m_accounts; + + //! The account that was selected when the user clicked OK. + MinecraftAccountPtr m_selected; + +private: + Ui::ProfileSelectDialog *ui; +}; diff --git a/ultimmc/launcher/ui/dialogs/ProfileSelectDialog.ui b/ultimmc/launcher/ui/dialogs/ProfileSelectDialog.ui new file mode 100644 index 0000000..e779b51 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ProfileSelectDialog.ui @@ -0,0 +1,62 @@ + + + ProfileSelectDialog + + + + 0 + 0 + 465 + 300 + + + + Select an Account + + + + + + Select a profile. + + + + + + + + 1 + + + + + + + + + + Use as default? + + + + + + + Use as default for this instance only? + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/ultimmc/launcher/ui/dialogs/ProfileSetupDialog.cpp b/ultimmc/launcher/ui/dialogs/ProfileSetupDialog.cpp new file mode 100644 index 0000000..76b6af4 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ProfileSetupDialog.cpp @@ -0,0 +1,256 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ProfileSetupDialog.h" +#include "ui_ProfileSetupDialog.h" + +#include +#include +#include +#include +#include + +#include "ui/dialogs/ProgressDialog.h" + +#include +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" + + +ProfileSetupDialog::ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget *parent) + : QDialog(parent), m_accountToSetup(accountToSetup), ui(new Ui::ProfileSetupDialog) +{ + ui->setupUi(this); + ui->errorLabel->setVisible(false); + + goodIcon = APPLICATION->getThemedIcon("status-good"); + yellowIcon = APPLICATION->getThemedIcon("status-yellow"); + badIcon = APPLICATION->getThemedIcon("status-bad"); + + QRegExp permittedNames("[a-zA-Z0-9_]{3,16}"); + auto nameEdit = ui->nameEdit; + nameEdit->setValidator(new QRegExpValidator(permittedNames)); + nameEdit->setClearButtonEnabled(true); + validityAction = nameEdit->addAction(yellowIcon, QLineEdit::LeadingPosition); + connect(nameEdit, &QLineEdit::textEdited, this, &ProfileSetupDialog::nameEdited); + + checkStartTimer.setSingleShot(true); + connect(&checkStartTimer, &QTimer::timeout, this, &ProfileSetupDialog::startCheck); + + setNameStatus(NameStatus::NotSet, QString()); +} + +ProfileSetupDialog::~ProfileSetupDialog() +{ + delete ui; +} + +void ProfileSetupDialog::on_buttonBox_accepted() +{ + setupProfile(currentCheck); +} + +void ProfileSetupDialog::on_buttonBox_rejected() +{ + reject(); +} + +void ProfileSetupDialog::setNameStatus(ProfileSetupDialog::NameStatus status, QString errorString = QString()) +{ + nameStatus = status; + auto okButton = ui->buttonBox->button(QDialogButtonBox::Ok); + switch(nameStatus) + { + case NameStatus::Available: { + validityAction->setIcon(goodIcon); + okButton->setEnabled(true); + } + break; + case NameStatus::NotSet: + case NameStatus::Pending: + validityAction->setIcon(yellowIcon); + okButton->setEnabled(false); + break; + case NameStatus::Exists: + case NameStatus::Error: + validityAction->setIcon(badIcon); + okButton->setEnabled(false); + break; + } + if(!errorString.isEmpty()) { + ui->errorLabel->setText(errorString); + ui->errorLabel->setVisible(true); + } + else { + ui->errorLabel->setVisible(false); + } +} + +void ProfileSetupDialog::nameEdited(const QString& name) +{ + if(!ui->nameEdit->hasAcceptableInput()) { + setNameStatus(NameStatus::NotSet, tr("Name is too short - must be between 3 and 16 characters long.")); + return; + } + scheduleCheck(name); +} + +void ProfileSetupDialog::scheduleCheck(const QString& name) { + queuedCheck = name; + setNameStatus(NameStatus::Pending); + checkStartTimer.start(1000); +} + +void ProfileSetupDialog::startCheck() { + if(isChecking) { + return; + } + if(queuedCheck.isNull()) { + return; + } + checkName(queuedCheck); +} + + +void ProfileSetupDialog::checkName(const QString &name) { + if(isChecking) { + return; + } + + currentCheck = name; + isChecking = true; + + auto token = m_accountToSetup->accessToken(); + + auto url = QString("https://api.minecraftservices.com/minecraft/profile/name/%1/available").arg(name); + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toUtf8()); + + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &ProfileSetupDialog::checkFinished); + requestor->get(request); +} + +void ProfileSetupDialog::checkFinished( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + + if(error == QNetworkReply::NoError) { + auto doc = QJsonDocument::fromJson(data); + auto root = doc.object(); + auto statusValue = root.value("status").toString("INVALID"); + if(statusValue == "AVAILABLE") { + setNameStatus(NameStatus::Available); + } + else if (statusValue == "DUPLICATE") { + setNameStatus(NameStatus::Exists, tr("Minecraft profile with name %1 already exists.").arg(currentCheck)); + } + else if (statusValue == "NOT_ALLOWED") { + setNameStatus(NameStatus::Exists, tr("The name %1 is not allowed.").arg(currentCheck)); + } + else { + setNameStatus(NameStatus::Error, tr("Unhandled profile name status: %1").arg(statusValue)); + } + } + else { + setNameStatus(NameStatus::Error, tr("Failed to check name availability.")); + } + isChecking = false; +} + +void ProfileSetupDialog::setupProfile(const QString &profileName) { + if(isWorking) { + return; + } + + auto token = m_accountToSetup->accessToken(); + + auto url = QString("https://api.minecraftservices.com/minecraft/profile"); + QNetworkRequest request = QNetworkRequest(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + request.setRawHeader("Accept", "application/json"); + request.setRawHeader("Authorization", QString("Bearer %1").arg(token).toUtf8()); + + QString payloadTemplate("{\"profileName\":\"%1\"}"); + auto data = payloadTemplate.arg(profileName).toUtf8(); + + AuthRequest *requestor = new AuthRequest(this); + connect(requestor, &AuthRequest::finished, this, &ProfileSetupDialog::setupProfileFinished); + requestor->post(request, data); + isWorking = true; + + auto button = ui->buttonBox->button(QDialogButtonBox::Cancel); + button->setEnabled(false); +} + +namespace { + +struct MojangError{ + static MojangError fromJSON(QByteArray data) { + MojangError out; + out.error = QString::fromUtf8(data); + auto doc = QJsonDocument::fromJson(data, &out.parseError); + auto object = doc.object(); + + out.fullyParsed = true; + out.fullyParsed &= Parsers::getString(object.value("path"), out.path); + out.fullyParsed &= Parsers::getString(object.value("error"), out.error); + out.fullyParsed &= Parsers::getString(object.value("errorMessage"), out.errorMessage); + + return out; + } + + QString rawError; + QJsonParseError parseError; + bool fullyParsed; + + QString path; + QString error; + QString errorMessage; +}; + +} + +void ProfileSetupDialog::setupProfileFinished( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + + isWorking = false; + if(error == QNetworkReply::NoError) { + /* + * data contains the profile in the response + * ... we could parse it and update the account, but let's just return back to the normal login flow instead... + */ + accept(); + } + else { + auto parsedError = MojangError::fromJSON(data); + ui->errorLabel->setVisible(true); + ui->errorLabel->setText(tr("The server returned the following error:") + "\n\n" + parsedError.errorMessage); + qDebug() << parsedError.rawError; + auto button = ui->buttonBox->button(QDialogButtonBox::Cancel); + button->setEnabled(true); + } +} diff --git a/ultimmc/launcher/ui/dialogs/ProfileSetupDialog.h b/ultimmc/launcher/ui/dialogs/ProfileSetupDialog.h new file mode 100644 index 0000000..6f413eb --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ProfileSetupDialog.h @@ -0,0 +1,88 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +#include +#include + +namespace Ui +{ +class ProfileSetupDialog; +} + +class ProfileSetupDialog : public QDialog +{ + Q_OBJECT +public: + + explicit ProfileSetupDialog(MinecraftAccountPtr accountToSetup, QWidget *parent = 0); + ~ProfileSetupDialog(); + + enum class NameStatus + { + NotSet, + Pending, + Available, + Exists, + Error + } nameStatus = NameStatus::NotSet; + +private slots: + void on_buttonBox_accepted(); + void on_buttonBox_rejected(); + + void nameEdited(const QString &name); + void checkFinished( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers + ); + void startCheck(); + + void setupProfileFinished( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers + ); +protected: + void scheduleCheck(const QString &name); + void checkName(const QString &name); + void setNameStatus(NameStatus status, QString errorString); + + void setupProfile(const QString & profileName); + +private: + MinecraftAccountPtr m_accountToSetup; + Ui::ProfileSetupDialog *ui; + QIcon goodIcon; + QIcon yellowIcon; + QIcon badIcon; + QAction * validityAction = nullptr; + + QString queuedCheck; + + bool isChecking = false; + bool isWorking = false; + QString currentCheck; + + QTimer checkStartTimer; +}; + diff --git a/ultimmc/launcher/ui/dialogs/ProfileSetupDialog.ui b/ultimmc/launcher/ui/dialogs/ProfileSetupDialog.ui new file mode 100644 index 0000000..9dbabb4 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ProfileSetupDialog.ui @@ -0,0 +1,74 @@ + + + ProfileSetupDialog + + + + 0 + 0 + 615 + 208 + + + + Choose Minecraft name + + + + + + + 0 + 0 + + + + You just need to take one more step to be able to play Minecraft on this account. + +Choose your name carefully: + + + true + + + nameEdit + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + true + + + Errors go here + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + nameEdit + + + + diff --git a/ultimmc/launcher/ui/dialogs/ProgressDialog.cpp b/ultimmc/launcher/ui/dialogs/ProgressDialog.cpp new file mode 100644 index 0000000..4b09285 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ProgressDialog.cpp @@ -0,0 +1,196 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ProgressDialog.h" +#include "ui_ProgressDialog.h" + +#include +#include + +#include "tasks/Task.h" + +ProgressDialog::ProgressDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ProgressDialog) +{ + ui->setupUi(this); + this->setWindowFlags(this->windowFlags() & ~Qt::WindowContextHelpButtonHint); + setSkipButton(false); + changeProgress(0, 100); +} + +void ProgressDialog::setSkipButton(bool present, QString label) +{ + ui->skipButton->setAutoDefault(false); + ui->skipButton->setDefault(false); + ui->skipButton->setFocusPolicy(Qt::ClickFocus); + ui->skipButton->setEnabled(present); + ui->skipButton->setVisible(present); + ui->skipButton->setText(label); + updateSize(); +} + +void ProgressDialog::on_skipButton_clicked(bool checked) +{ + Q_UNUSED(checked); + task->abort(); +} + +ProgressDialog::~ProgressDialog() +{ + delete ui; +} + +void ProgressDialog::updateSize() +{ + QSize qSize = QSize(480, minimumSizeHint().height()); + resize(qSize); + setFixedSize(qSize); +} + +int ProgressDialog::execWithTask(Task *task) +{ + this->task = task; + QDialog::DialogCode result; + + if(!task) + { + qDebug() << "Programmer error: progress dialog created with null task."; + return Accepted; + } + + if(handleImmediateResult(result)) + { + return result; + } + + // Connect signals. + connect(task, SIGNAL(started()), SLOT(onTaskStarted())); + connect(task, SIGNAL(failed(QString)), SLOT(onTaskFailed(QString))); + connect(task, SIGNAL(succeeded()), SLOT(onTaskSucceeded())); + connect(task, SIGNAL(status(QString)), SLOT(changeStatus(const QString &))); + connect(task, SIGNAL(progress(qint64, qint64)), SLOT(changeProgress(qint64, qint64))); + + // if this didn't connect to an already running task, invoke start + if(!task->isRunning()) + { + task->start(); + } + if(task->isRunning()) + { + changeProgress(task->getProgress(), task->getTotalProgress()); + changeStatus(task->getStatus()); + return QDialog::exec(); + } + else if(handleImmediateResult(result)) + { + return result; + } + else + { + return QDialog::Rejected; + } +} + +// TODO: only provide the unique_ptr overloads +int ProgressDialog::execWithTask(std::unique_ptr &&task) +{ + connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); + return execWithTask(task.release()); +} +int ProgressDialog::execWithTask(std::unique_ptr &task) +{ + connect(this, &ProgressDialog::destroyed, task.get(), &Task::deleteLater); + return execWithTask(task.release()); +} + +bool ProgressDialog::handleImmediateResult(QDialog::DialogCode &result) +{ + if(task->isFinished()) + { + if(task->wasSuccessful()) + { + result = QDialog::Accepted; + } + else + { + result = QDialog::Rejected; + } + return true; + } + return false; +} + +Task *ProgressDialog::getTask() +{ + return task; +} + +void ProgressDialog::onTaskStarted() +{ +} + +void ProgressDialog::onTaskFailed(QString failure) +{ + reject(); +} + +void ProgressDialog::onTaskSucceeded() +{ + accept(); +} + +void ProgressDialog::changeStatus(const QString &status) +{ + ui->statusLabel->setText(status); + updateSize(); +} + +void ProgressDialog::changeProgress(qint64 current, qint64 total) +{ + ui->taskProgressBar->setMaximum(total); + ui->taskProgressBar->setValue(current); +} + +void ProgressDialog::keyPressEvent(QKeyEvent *e) +{ + if(ui->skipButton->isVisible()) + { + if (e->key() == Qt::Key_Escape) + { + on_skipButton_clicked(true); + return; + } + else if(e->key() == Qt::Key_Tab) + { + ui->skipButton->setFocusPolicy(Qt::StrongFocus); + ui->skipButton->setFocus(); + ui->skipButton->setAutoDefault(true); + ui->skipButton->setDefault(true); + return; + } + } + QDialog::keyPressEvent(e); +} + +void ProgressDialog::closeEvent(QCloseEvent *e) +{ + if (task && task->isRunning()) + { + e->ignore(); + } + else + { + QDialog::closeEvent(e); + } +} diff --git a/ultimmc/launcher/ui/dialogs/ProgressDialog.h b/ultimmc/launcher/ui/dialogs/ProgressDialog.h new file mode 100644 index 0000000..b28ad4f --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ProgressDialog.h @@ -0,0 +1,71 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +class Task; + +namespace Ui +{ +class ProgressDialog; +} + +class ProgressDialog : public QDialog +{ + Q_OBJECT + +public: + explicit ProgressDialog(QWidget *parent = 0); + ~ProgressDialog(); + + void updateSize(); + + int execWithTask(Task *task); + int execWithTask(std::unique_ptr &&task); + int execWithTask(std::unique_ptr &task); + + void setSkipButton(bool present, QString label = QString()); + + Task *getTask(); + +public +slots: + void onTaskStarted(); + void onTaskFailed(QString failure); + void onTaskSucceeded(); + + void changeStatus(const QString &status); + void changeProgress(qint64 current, qint64 total); + + +private +slots: + void on_skipButton_clicked(bool checked); + +protected: + virtual void keyPressEvent(QKeyEvent *e); + virtual void closeEvent(QCloseEvent *e); + +private: + bool handleImmediateResult(QDialog::DialogCode &result); + +private: + Ui::ProgressDialog *ui; + + Task *task; +}; diff --git a/ultimmc/launcher/ui/dialogs/ProgressDialog.ui b/ultimmc/launcher/ui/dialogs/ProgressDialog.ui new file mode 100644 index 0000000..04b8fef --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/ProgressDialog.ui @@ -0,0 +1,66 @@ + + + ProgressDialog + + + + 0 + 0 + 400 + 100 + + + + + 400 + 0 + + + + + 600 + 16777215 + + + + Please wait... + + + + + + Task Status... + + + true + + + + + + + 24 + + + false + + + + + + + + 0 + 0 + + + + Skip + + + + + + + + diff --git a/ultimmc/launcher/ui/dialogs/SkinUploadDialog.cpp b/ultimmc/launcher/ui/dialogs/SkinUploadDialog.cpp new file mode 100644 index 0000000..6a5a324 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/SkinUploadDialog.cpp @@ -0,0 +1,146 @@ +#include +#include +#include + +#include + +#include +#include +#include + +#include "SkinUploadDialog.h" +#include "ui_SkinUploadDialog.h" +#include "ProgressDialog.h" +#include "CustomMessageBox.h" + +void SkinUploadDialog::on_buttonBox_rejected() +{ + close(); +} + +void SkinUploadDialog::on_buttonBox_accepted() +{ + QString fileName; + QString input = ui->skinPathTextBox->text(); + QRegExp urlPrefixMatcher("^([a-z]+)://.+$"); + bool isLocalFile = false; + // it has an URL prefix -> it is an URL + if(urlPrefixMatcher.exactMatch(input)) + { + QUrl fileURL = input; + if(fileURL.isValid()) + { + // local? + if(fileURL.isLocalFile()) + { + isLocalFile = true; + fileName = fileURL.toLocalFile(); + } + else + { + CustomMessageBox::selectable( + this, + tr("Skin Upload"), + tr("Using remote URLs for setting skins is not implemented yet."), + QMessageBox::Warning + )->exec(); + close(); + return; + } + } + else + { + CustomMessageBox::selectable( + this, + tr("Skin Upload"), + tr("You cannot use an invalid URL for uploading skins."), + QMessageBox::Warning + )->exec(); + close(); + return; + } + } + else + { + // just assume it's a path then + isLocalFile = true; + fileName = ui->skinPathTextBox->text(); + } + if (isLocalFile && !QFile::exists(fileName)) + { + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Skin file does not exist!"), QMessageBox::Warning)->exec(); + close(); + return; + } + SkinUpload::Model model = SkinUpload::STEVE; + if (ui->steveBtn->isChecked()) + { + model = SkinUpload::STEVE; + } + else if (ui->alexBtn->isChecked()) + { + model = SkinUpload::ALEX; + } + ProgressDialog prog(this); + SequentialTask skinUpload; + skinUpload.addTask(shared_qobject_ptr(new SkinUpload(this, m_acct->accessToken(), FS::read(fileName), model))); + auto selectedCape = ui->capeCombo->currentData().toString(); + if(selectedCape != m_acct->accountData()->minecraftProfile.currentCape) { + skinUpload.addTask(shared_qobject_ptr(new CapeChange(this, m_acct->accessToken(), selectedCape))); + } + if (prog.execWithTask(&skinUpload) != QDialog::Accepted) + { + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Failed to upload skin!"), QMessageBox::Warning)->exec(); + close(); + return; + } + CustomMessageBox::selectable(this, tr("Skin Upload"), tr("Success"), QMessageBox::Information)->exec(); + close(); +} + +void SkinUploadDialog::on_skinBrowseBtn_clicked() +{ + QString raw_path = QFileDialog::getOpenFileName(this, tr("Select Skin Texture"), QString(), "*.png"); + if (raw_path.isEmpty() || !QFileInfo::exists(raw_path)) + { + return; + } + QString cooked_path = FS::NormalizePath(raw_path); + ui->skinPathTextBox->setText(cooked_path); +} + +SkinUploadDialog::SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent) + :QDialog(parent), m_acct(acct), ui(new Ui::SkinUploadDialog) +{ + ui->setupUi(this); + + // FIXME: add a model for this, download/refresh the capes on demand + auto &data = *acct->accountData(); + int index = 0; + ui->capeCombo->addItem(tr("No Cape"), QVariant()); + auto currentCape = data.minecraftProfile.currentCape; + if(currentCape.isEmpty()) { + ui->capeCombo->setCurrentIndex(index); + } + + for(auto & cape: data.minecraftProfile.capes) { + index++; + if(cape.data.size()) { + QPixmap capeImage; + if(capeImage.loadFromData(cape.data, "PNG")) { + QPixmap preview = QPixmap(10, 16); + QPainter painter(&preview); + painter.drawPixmap(0, 0, capeImage.copy(1, 1, 10, 16)); + ui->capeCombo->addItem(capeImage, cape.alias, cape.id); + if(currentCape == cape.id) { + ui->capeCombo->setCurrentIndex(index); + } + continue; + } + } + ui->capeCombo->addItem(cape.alias, cape.id); + if(currentCape == cape.id) { + ui->capeCombo->setCurrentIndex(index); + } + } +} diff --git a/ultimmc/launcher/ui/dialogs/SkinUploadDialog.h b/ultimmc/launcher/ui/dialogs/SkinUploadDialog.h new file mode 100644 index 0000000..84d17dc --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/SkinUploadDialog.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace Ui +{ + class SkinUploadDialog; +} + +class SkinUploadDialog : public QDialog { + Q_OBJECT +public: + explicit SkinUploadDialog(MinecraftAccountPtr acct, QWidget *parent = 0); + virtual ~SkinUploadDialog() {}; + +public slots: + void on_buttonBox_accepted(); + + void on_buttonBox_rejected(); + + void on_skinBrowseBtn_clicked(); + +protected: + MinecraftAccountPtr m_acct; + +private: + Ui::SkinUploadDialog *ui; +}; diff --git a/ultimmc/launcher/ui/dialogs/SkinUploadDialog.ui b/ultimmc/launcher/ui/dialogs/SkinUploadDialog.ui new file mode 100644 index 0000000..f4b0ed0 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/SkinUploadDialog.ui @@ -0,0 +1,97 @@ + + + SkinUploadDialog + + + + 0 + 0 + 394 + 360 + + + + Skin Upload + + + + + + Skin File + + + + + + + + + + 0 + 0 + + + + + 28 + 16777215 + + + + ... + + + + + + + + + + Player Model + + + + + + Steve Model + + + true + + + + + + + Alex Model + + + + + + + + + + Cape + + + + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/ultimmc/launcher/ui/dialogs/UpdateDialog.cpp b/ultimmc/launcher/ui/dialogs/UpdateDialog.cpp new file mode 100644 index 0000000..f949ebe --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/UpdateDialog.cpp @@ -0,0 +1,171 @@ +#include "UpdateDialog.h" +#include "ui_UpdateDialog.h" +#include +#include "Application.h" +#include +#include + +#include "BuildConfig.h" +#include "HoeDown.h" + +UpdateDialog::UpdateDialog(bool hasUpdate, QWidget *parent) : QDialog(parent), ui(new Ui::UpdateDialog) +{ + ui->setupUi(this); + if(hasUpdate) + { + ui->label->setText(tr("A new update is available!")); + } + else + { + ui->label->setText(tr("No updates found. You are running the latest version.")); + ui->btnUpdateNow->setHidden(true); + ui->btnUpdateLater->setText(tr("Close")); + } + ui->changelogBrowser->setHtml(tr("

Loading changelog...

")); + loadChangelog(); + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("UpdateDialogGeometry").toByteArray())); +} + +UpdateDialog::~UpdateDialog() +{ +} + +void UpdateDialog::loadChangelog() +{ + dljob = new NetJob("Changelog", APPLICATION->network()); + QString url; + url = QString("https://api.github.com/repos/MultiMC/Launcher/compare/%1...develop").arg(BuildConfig.GIT_COMMIT); + m_changelogType = CHANGELOG_COMMITS; + dljob->addNetAction(Net::Download::makeByteArray(QUrl(url), &changelogData)); + connect(dljob.get(), &NetJob::succeeded, this, &UpdateDialog::changelogLoaded); + connect(dljob.get(), &NetJob::failed, this, &UpdateDialog::changelogFailed); + dljob->start(); +} + +QString reprocessMarkdown(QByteArray markdown) +{ + HoeDown hoedown; + QString output = hoedown.process(markdown); + + // HACK: easier than customizing hoedown + output.replace(QRegExp("GH-([0-9]+)"), "GH-\\1"); + qDebug() << output; + return output; +} + +QString reprocessCommits(QByteArray json) +{ + try + { + QString result; + auto document = Json::requireDocument(json); + auto rootobject = Json::requireObject(document); + auto status = Json::requireString(rootobject, "status"); + auto diff_url = Json::requireString(rootobject, "html_url"); + + auto print_commits = [&]() + { + result += ""; + auto commitarray = Json::requireArray(rootobject, "commits"); + for(int i = commitarray.size() - 1; i >= 0; i--) + { + const auto & commitval = commitarray[i]; + auto commitobj = Json::requireValueObject(commitval); + auto parents_info = Json::ensureArray(commitobj, "parents"); + // NOTE: this ignores merge commits, because they have more than one parent + if(parents_info.size() > 1) + { + continue; + } + auto commit_url = Json::requireString(commitobj, "html_url"); + auto commit_info = Json::requireObject(commitobj, "commit"); + auto commit_message = Json::requireString(commit_info, "message"); + auto lines = commit_message.split('\n'); + QRegularExpression regexp("(?(GH-(?[0-9]+))|(NOISSUE)|(SCRATCH))? *(?.*) *"); + auto match = regexp.match(lines.takeFirst(), 0, QRegularExpression::NormalMatch); + auto issuenr = match.captured("issuenr"); + auto prefix = match.captured("prefix"); + auto rest = match.captured("rest"); + result += ""; + lines.prepend(rest); + result += ""; + } + result += "
"; + if(issuenr.length()) + { + result += QString("GH-%2").arg(issuenr, issuenr); + } + else if(prefix.length()) + { + result += QString("%2").arg(commit_url, prefix); + } + else + { + result += QString("NOISSUE").arg(commit_url); + } + result += "

" + lines.join("
") + "

"; + }; + + if(status == "identical") + { + return QObject::tr("

There are no code changes between your current version and the latest.

"); + } + else if(status == "ahead") + { + result += QObject::tr("

Following commits were added since last update:

"); + print_commits(); + } + else if(status == "diverged") + { + auto commit_ahead = Json::requireInteger(rootobject, "ahead_by"); + auto commit_behind = Json::requireInteger(rootobject, "behind_by"); + result += QObject::tr("

The update removes %1 commits and adds the following %2:

").arg(commit_behind).arg(commit_ahead); + print_commits(); + } + result += QObject::tr("

You can look at the changes on github.

").arg(diff_url); + return result; + } + catch (const JSONValidationError &e) + { + qWarning() << "Got an unparseable commit log from github:" << e.what(); + qDebug() << json; + } + return QString(); +} + +void UpdateDialog::changelogLoaded() +{ + QString result; + switch(m_changelogType) + { + case CHANGELOG_COMMITS: + result = reprocessCommits(changelogData); + break; + case CHANGELOG_MARKDOWN: + result = reprocessMarkdown(changelogData); + break; + } + changelogData.clear(); + ui->changelogBrowser->setHtml(result); +} + +void UpdateDialog::changelogFailed(QString reason) +{ + ui->changelogBrowser->setHtml(tr("

Failed to fetch changelog... Error: %1

").arg(reason)); +} + +void UpdateDialog::on_btnUpdateLater_clicked() +{ + reject(); +} + +void UpdateDialog::on_btnUpdateNow_clicked() +{ + done(UPDATE_NOW); +} + +void UpdateDialog::closeEvent(QCloseEvent* evt) +{ + APPLICATION->settings()->set("UpdateDialogGeometry", saveGeometry().toBase64()); + QDialog::closeEvent(evt); +} diff --git a/ultimmc/launcher/ui/dialogs/UpdateDialog.h b/ultimmc/launcher/ui/dialogs/UpdateDialog.h new file mode 100644 index 0000000..07cbe09 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/UpdateDialog.h @@ -0,0 +1,67 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "net/NetJob.h" + +namespace Ui +{ +class UpdateDialog; +} + +enum UpdateAction +{ + UPDATE_LATER = QDialog::Rejected, + UPDATE_NOW = QDialog::Accepted, +}; + +enum ChangelogType +{ + CHANGELOG_MARKDOWN, + CHANGELOG_COMMITS +}; + +class UpdateDialog : public QDialog +{ + Q_OBJECT + +public: + explicit UpdateDialog(bool hasUpdate = true, QWidget *parent = 0); + ~UpdateDialog(); + +public slots: + void on_btnUpdateNow_clicked(); + void on_btnUpdateLater_clicked(); + + /// Starts loading the changelog + void loadChangelog(); + + /// Slot for when the chengelog loads successfully. + void changelogLoaded(); + + /// Slot for when the chengelog fails to load... + void changelogFailed(QString reason); + +protected: + void closeEvent(QCloseEvent * ) override; + +private: + Ui::UpdateDialog *ui; + QByteArray changelogData; + NetJob::Ptr dljob; + ChangelogType m_changelogType = CHANGELOG_MARKDOWN; +}; diff --git a/ultimmc/launcher/ui/dialogs/UpdateDialog.ui b/ultimmc/launcher/ui/dialogs/UpdateDialog.ui new file mode 100644 index 0000000..b0b3dd8 --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/UpdateDialog.ui @@ -0,0 +1,91 @@ + + + UpdateDialog + + + + 0 + 0 + 657 + 673 + + + + MultiMC Update + + + + :/icons/toolbar/checkupdate:/icons/toolbar/checkupdate + + + + + + + + + 14 + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + changelogBrowser + + + + + + + + + true + + + + + + + + + + 0 + 0 + + + + Update now + + + + + + + + 0 + 0 + + + + Don't update yet + + + + + + + + + changelogBrowser + btnUpdateNow + btnUpdateLater + + + + + + diff --git a/ultimmc/launcher/ui/dialogs/VersionSelectDialog.cpp b/ultimmc/launcher/ui/dialogs/VersionSelectDialog.cpp new file mode 100644 index 0000000..70ef72d --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/VersionSelectDialog.cpp @@ -0,0 +1,141 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "VersionSelectDialog.h" + +#include +#include +#include +#include +#include +#include + +#include "ui/dialogs/ProgressDialog.h" +#include "ui/widgets/VersionSelectWidget.h" +#include "ui/dialogs/CustomMessageBox.h" + +#include "BaseVersion.h" +#include "BaseVersionList.h" +#include "tasks/Task.h" +#include "Application.h" +#include "VersionProxyModel.h" + +VersionSelectDialog::VersionSelectDialog(BaseVersionList *vlist, QString title, QWidget *parent, bool cancelable) + : QDialog(parent) +{ + setObjectName(QStringLiteral("VersionSelectDialog")); + resize(400, 347); + m_verticalLayout = new QVBoxLayout(this); + m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + + m_versionWidget = new VersionSelectWidget(parent); + m_verticalLayout->addWidget(m_versionWidget); + + m_horizontalLayout = new QHBoxLayout(); + m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); + + m_refreshButton = new QPushButton(this); + m_refreshButton->setObjectName(QStringLiteral("refreshButton")); + m_horizontalLayout->addWidget(m_refreshButton); + + m_buttonBox = new QDialogButtonBox(this); + m_buttonBox->setObjectName(QStringLiteral("buttonBox")); + m_buttonBox->setOrientation(Qt::Horizontal); + m_buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok); + m_horizontalLayout->addWidget(m_buttonBox); + + m_verticalLayout->addLayout(m_horizontalLayout); + + retranslate(); + + QObject::connect(m_buttonBox, SIGNAL(accepted()), this, SLOT(accept())); + QObject::connect(m_buttonBox, SIGNAL(rejected()), this, SLOT(reject())); + + QMetaObject::connectSlotsByName(this); + setWindowModality(Qt::WindowModal); + setWindowTitle(title); + + m_vlist = vlist; + + if (!cancelable) + { + m_buttonBox->button(QDialogButtonBox::Cancel)->setEnabled(false); + } +} + +void VersionSelectDialog::retranslate() +{ + // FIXME: overrides custom title given in constructor! + setWindowTitle(tr("Choose Version")); + m_refreshButton->setToolTip(tr("Reloads the version list.")); + m_refreshButton->setText(tr("&Refresh")); +} + +void VersionSelectDialog::setCurrentVersion(const QString& version) +{ + m_currentVersion = version; + m_versionWidget->setCurrentVersion(version); +} + +void VersionSelectDialog::setEmptyString(QString emptyString) +{ + m_versionWidget->setEmptyString(emptyString); +} + +void VersionSelectDialog::setEmptyErrorString(QString emptyErrorString) +{ + m_versionWidget->setEmptyErrorString(emptyErrorString); +} + +void VersionSelectDialog::setResizeOn(int column) +{ + resizeOnColumn = column; +} + +int VersionSelectDialog::exec() +{ + QDialog::open(); + m_versionWidget->initialize(m_vlist); + if(resizeOnColumn != -1) + { + m_versionWidget->setResizeOn(resizeOnColumn); + } + return QDialog::exec(); +} + +void VersionSelectDialog::selectRecommended() +{ + m_versionWidget->selectRecommended(); +} + +BaseVersionPtr VersionSelectDialog::selectedVersion() const +{ + return m_versionWidget->selectedVersion(); +} + +void VersionSelectDialog::on_refreshButton_clicked() +{ + m_versionWidget->loadList(); +} + +void VersionSelectDialog::setExactFilter(BaseVersionList::ModelRoles role, QString filter) +{ + m_versionWidget->setExactFilter(role, filter); +} + +void VersionSelectDialog::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter) +{ + m_versionWidget->setFuzzyFilter(role, filter); +} diff --git a/ultimmc/launcher/ui/dialogs/VersionSelectDialog.h b/ultimmc/launcher/ui/dialogs/VersionSelectDialog.h new file mode 100644 index 0000000..ed30d3f --- /dev/null +++ b/ultimmc/launcher/ui/dialogs/VersionSelectDialog.h @@ -0,0 +1,78 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + + +#include "BaseVersionList.h" + +class QVBoxLayout; +class QHBoxLayout; +class QDialogButtonBox; +class VersionSelectWidget; +class QPushButton; + +namespace Ui +{ +class VersionSelectDialog; +} + +class VersionProxyModel; + +class VersionSelectDialog : public QDialog +{ + Q_OBJECT + +public: + explicit VersionSelectDialog(BaseVersionList *vlist, QString title, QWidget *parent = 0, bool cancelable = true); + virtual ~VersionSelectDialog() {}; + + int exec() override; + + BaseVersionPtr selectedVersion() const; + + void setCurrentVersion(const QString & version); + void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter); + void setExactFilter(BaseVersionList::ModelRoles role, QString filter); + void setEmptyString(QString emptyString); + void setEmptyErrorString(QString emptyErrorString); + void setResizeOn(int column); + +private slots: + void on_refreshButton_clicked(); + +private: + void retranslate(); + void selectRecommended(); + +private: + QString m_currentVersion; + VersionSelectWidget *m_versionWidget = nullptr; + QVBoxLayout *m_verticalLayout = nullptr; + QHBoxLayout *m_horizontalLayout = nullptr; + QPushButton *m_refreshButton = nullptr; + QDialogButtonBox *m_buttonBox = nullptr; + + BaseVersionList *m_vlist = nullptr; + + VersionProxyModel *m_proxyModel = nullptr; + + int resizeOnColumn = -1; + + Task * loadTask = nullptr; +}; diff --git a/ultimmc/launcher/ui/instanceview/AccessibleInstanceView.cpp b/ultimmc/launcher/ui/instanceview/AccessibleInstanceView.cpp new file mode 100644 index 0000000..7de3ac7 --- /dev/null +++ b/ultimmc/launcher/ui/instanceview/AccessibleInstanceView.cpp @@ -0,0 +1,778 @@ +#include "InstanceView.h" +#include "AccessibleInstanceView.h" +#include "AccessibleInstanceView_p.h" + +#include +#include +#include + +#ifndef QT_NO_ACCESSIBILITY + +QAccessibleInterface *groupViewAccessibleFactory(const QString &classname, QObject *object) +{ + QAccessibleInterface *iface = 0; + if (!object || !object->isWidgetType()) + return iface; + + QWidget *widget = static_cast(object); + + if (classname == QLatin1String("InstanceView")) { + iface = new AccessibleInstanceView((InstanceView *)widget); + } + return iface; +} + + +QAbstractItemView *AccessibleInstanceView::view() const +{ + return qobject_cast(object()); +} + +int AccessibleInstanceView::logicalIndex(const QModelIndex &index) const +{ + if (!view()->model() || !index.isValid()) + return -1; + return index.row() * (index.model()->columnCount()) + index.column(); +} + +AccessibleInstanceView::AccessibleInstanceView(QWidget *w) + : QAccessibleObject(w) +{ + Q_ASSERT(view()); +} + +bool AccessibleInstanceView::isValid() const +{ + return view(); +} + +AccessibleInstanceView::~AccessibleInstanceView() +{ + for (QAccessible::Id id : childToId) { + QAccessible::deleteAccessibleInterface(id); + } +} + +QAccessibleInterface *AccessibleInstanceView::cellAt(int row, int column) const +{ + if (!view()->model()) { + return 0; + } + + QModelIndex index = view()->model()->index(row, column, view()->rootIndex()); + if (Q_UNLIKELY(!index.isValid())) { + qWarning() << "AccessibleInstanceView::cellAt: invalid index: " << index << " for " << view(); + return 0; + } + + return child(logicalIndex(index)); +} + +QAccessibleInterface *AccessibleInstanceView::caption() const +{ + return 0; +} + +QString AccessibleInstanceView::columnDescription(int column) const +{ + if (!view()->model()) + return QString(); + + return view()->model()->headerData(column, Qt::Horizontal).toString(); +} + +int AccessibleInstanceView::columnCount() const +{ + if (!view()->model()) + return 0; + return 1; +} + +int AccessibleInstanceView::rowCount() const +{ + if (!view()->model()) + return 0; + return view()->model()->rowCount(); +} + +int AccessibleInstanceView::selectedCellCount() const +{ + if (!view()->selectionModel()) + return 0; + return view()->selectionModel()->selectedIndexes().count(); +} + +int AccessibleInstanceView::selectedColumnCount() const +{ + if (!view()->selectionModel()) + return 0; + return view()->selectionModel()->selectedColumns().count(); +} + +int AccessibleInstanceView::selectedRowCount() const +{ + if (!view()->selectionModel()) + return 0; + return view()->selectionModel()->selectedRows().count(); +} + +QString AccessibleInstanceView::rowDescription(int row) const +{ + if (!view()->model()) + return QString(); + return view()->model()->headerData(row, Qt::Vertical).toString(); +} + +QList AccessibleInstanceView::selectedCells() const +{ + QList cells; + if (!view()->selectionModel()) + return cells; + const QModelIndexList selectedIndexes = view()->selectionModel()->selectedIndexes(); + cells.reserve(selectedIndexes.size()); + for (const QModelIndex &index : selectedIndexes) + cells.append(child(logicalIndex(index))); + return cells; +} + +QList AccessibleInstanceView::selectedColumns() const +{ + if (!view()->selectionModel()) { + return QList(); + } + + const QModelIndexList selectedColumns = view()->selectionModel()->selectedColumns(); + + QList columns; + columns.reserve(selectedColumns.size()); + for (const QModelIndex &index : selectedColumns) { + columns.append(index.column()); + } + + return columns; +} + +QList AccessibleInstanceView::selectedRows() const +{ + if (!view()->selectionModel()) { + return QList(); + } + + QList rows; + + const QModelIndexList selectedRows = view()->selectionModel()->selectedRows(); + + rows.reserve(selectedRows.size()); + for (const QModelIndex &index : selectedRows) { + rows.append(index.row()); + } + + return rows; +} + +QAccessibleInterface *AccessibleInstanceView::summary() const +{ + return 0; +} + +bool AccessibleInstanceView::isColumnSelected(int column) const +{ + if (!view()->selectionModel()) { + return false; + } + + return view()->selectionModel()->isColumnSelected(column, QModelIndex()); +} + +bool AccessibleInstanceView::isRowSelected(int row) const +{ + if (!view()->selectionModel()) { + return false; + } + + return view()->selectionModel()->isRowSelected(row, QModelIndex()); +} + +bool AccessibleInstanceView::selectRow(int row) +{ + if (!view()->model() || !view()->selectionModel()) { + return false; + } + QModelIndex index = view()->model()->index(row, 0, view()->rootIndex()); + + if (!index.isValid() || view()->selectionBehavior() == QAbstractItemView::SelectColumns) { + return false; + } + + switch (view()->selectionMode()) { + case QAbstractItemView::NoSelection: { + return false; + } + case QAbstractItemView::SingleSelection: { + if (view()->selectionBehavior() != QAbstractItemView::SelectRows && columnCount() > 1 ) + return false; + view()->clearSelection(); + break; + } + case QAbstractItemView::ContiguousSelection: { + if ((!row || !view()->selectionModel()->isRowSelected(row - 1, view()->rootIndex())) && !view()->selectionModel()->isRowSelected(row + 1, view()->rootIndex())) { + view()->clearSelection(); + } + break; + } + default: { + break; + } + } + + view()->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Rows); + return true; +} + +bool AccessibleInstanceView::selectColumn(int column) +{ + if (!view()->model() || !view()->selectionModel()) { + return false; + } + QModelIndex index = view()->model()->index(0, column, view()->rootIndex()); + + if (!index.isValid() || view()->selectionBehavior() == QAbstractItemView::SelectRows) { + return false; + } + + switch (view()->selectionMode()) { + case QAbstractItemView::NoSelection: { + return false; + } + case QAbstractItemView::SingleSelection: { + if (view()->selectionBehavior() != QAbstractItemView::SelectColumns && rowCount() > 1) { + return false; + } + // fallthrough intentional + } + case QAbstractItemView::ContiguousSelection: { + if ((!column || !view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) && !view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) { + view()->clearSelection(); + } + break; + } + default: { + break; + } + } + + view()->selectionModel()->select(index, QItemSelectionModel::Select | QItemSelectionModel::Columns); + return true; +} + +bool AccessibleInstanceView::unselectRow(int row) +{ + if (!view()->model() || !view()->selectionModel()) { + return false; + } + + QModelIndex index = view()->model()->index(row, 0, view()->rootIndex()); + if (!index.isValid()) { + return false; + } + + QItemSelection selection(index, index); + auto selectionModel = view()->selectionModel(); + + switch (view()->selectionMode()) { + case QAbstractItemView::SingleSelection: + // no unselect + if (selectedRowCount() == 1) { + return false; + } + break; + case QAbstractItemView::ContiguousSelection: { + // no unselect + if (selectedRowCount() == 1) { + return false; + } + + + if ((!row || selectionModel->isRowSelected(row - 1, view()->rootIndex())) && selectionModel->isRowSelected(row + 1, view()->rootIndex())) { + //If there are rows selected both up the current row and down the current rown, + //the ones which are down the current row will be deselected + selection = QItemSelection(index, view()->model()->index(rowCount() - 1, 0, view()->rootIndex())); + } + } + default: { + break; + } + } + + selectionModel->select(selection, QItemSelectionModel::Deselect | QItemSelectionModel::Rows); + return true; +} + +bool AccessibleInstanceView::unselectColumn(int column) +{ + auto model = view()->model(); + if (!model || !view()->selectionModel()) { + return false; + } + + QModelIndex index = model->index(0, column, view()->rootIndex()); + if (!index.isValid()) { + return false; + } + + QItemSelection selection(index, index); + + switch (view()->selectionMode()) { + case QAbstractItemView::SingleSelection: { + //In SingleSelection and ContiguousSelection once an item + //is selected, there's no way for the user to unselect all items + if (selectedColumnCount() == 1) { + return false; + } + break; + } + case QAbstractItemView::ContiguousSelection: + if (selectedColumnCount() == 1) { + return false; + } + + if ((!column || view()->selectionModel()->isColumnSelected(column - 1, view()->rootIndex())) + && view()->selectionModel()->isColumnSelected(column + 1, view()->rootIndex())) { + //If there are columns selected both at the left of the current row and at the right + //of the current row, the ones which are at the right will be deselected + selection = QItemSelection(index, model->index(0, columnCount() - 1, view()->rootIndex())); + } + default: + break; + } + + view()->selectionModel()->select(selection, QItemSelectionModel::Deselect | QItemSelectionModel::Columns); + return true; +} + +QAccessible::Role AccessibleInstanceView::role() const +{ + return QAccessible::List; +} + +QAccessible::State AccessibleInstanceView::state() const +{ + return QAccessible::State(); +} + +QAccessibleInterface *AccessibleInstanceView::childAt(int x, int y) const +{ + QPoint viewportOffset = view()->viewport()->mapTo(view(), QPoint(0,0)); + QPoint indexPosition = view()->mapFromGlobal(QPoint(x, y) - viewportOffset); + // FIXME: if indexPosition < 0 in one coordinate, return header + + QModelIndex index = view()->indexAt(indexPosition); + if (index.isValid()) { + return child(logicalIndex(index)); + } + return 0; +} + +int AccessibleInstanceView::childCount() const +{ + if (!view()->model()) { + return 0; + } + return (view()->model()->rowCount()) * (view()->model()->columnCount()); +} + +int AccessibleInstanceView::indexOfChild(const QAccessibleInterface *iface) const +{ + if (!view()->model()) + return -1; + QAccessibleInterface *parent = iface->parent(); + if (parent->object() != view()) + return -1; + + Q_ASSERT(iface->role() != QAccessible::TreeItem); // should be handled by tree class + if (iface->role() == QAccessible::Cell || iface->role() == QAccessible::ListItem) { + const AccessibleInstanceViewItem* cell = static_cast(iface); + return logicalIndex(cell->m_index); + } else if (iface->role() == QAccessible::Pane) { + return 0; // corner button + } else { + qWarning() << "AccessibleInstanceView::indexOfChild has a child with unknown role..." << iface->role() << iface->text(QAccessible::Name); + } + // FIXME: we are in denial of our children. this should stop. + return -1; +} + +QString AccessibleInstanceView::text(QAccessible::Text t) const +{ + if (t == QAccessible::Description) + return view()->accessibleDescription(); + return view()->accessibleName(); +} + +QRect AccessibleInstanceView::rect() const +{ + if (!view()->isVisible()) + return QRect(); + QPoint pos = view()->mapToGlobal(QPoint(0, 0)); + return QRect(pos.x(), pos.y(), view()->width(), view()->height()); +} + +QAccessibleInterface *AccessibleInstanceView::parent() const +{ + if (view() && view()->parent()) { + if (qstrcmp("QComboBoxPrivateContainer", view()->parent()->metaObject()->className()) == 0) { + return QAccessible::queryAccessibleInterface(view()->parent()->parent()); + } + return QAccessible::queryAccessibleInterface(view()->parent()); + } + return 0; +} + +QAccessibleInterface *AccessibleInstanceView::child(int logicalIndex) const +{ + if (!view()->model()) + return 0; + + auto id = childToId.constFind(logicalIndex); + if (id != childToId.constEnd()) + return QAccessible::accessibleInterface(id.value()); + + int columns = view()->model()->columnCount(); + + int row = logicalIndex / columns; + int column = logicalIndex % columns; + + QAccessibleInterface *iface = 0; + + QModelIndex index = view()->model()->index(row, column, view()->rootIndex()); + if (Q_UNLIKELY(!index.isValid())) { + qWarning("AccessibleInstanceView::child: Invalid index at: %d %d", row, column); + return 0; + } + iface = new AccessibleInstanceViewItem(view(), index); + + QAccessible::registerAccessibleInterface(iface); + childToId.insert(logicalIndex, QAccessible::uniqueId(iface)); + return iface; +} + +void *AccessibleInstanceView::interface_cast(QAccessible::InterfaceType t) +{ + if (t == QAccessible::TableInterface) + return static_cast(this); + return 0; +} + +void AccessibleInstanceView::modelChange(QAccessibleTableModelChangeEvent *event) +{ + // if there is no cache yet, we don't update anything + if (childToId.isEmpty()) + return; + + switch (event->modelChangeType()) { + case QAccessibleTableModelChangeEvent::ModelReset: + for (QAccessible::Id id : childToId) + QAccessible::deleteAccessibleInterface(id); + childToId.clear(); + break; + + // rows are inserted: move every row after that + case QAccessibleTableModelChangeEvent::RowsInserted: + case QAccessibleTableModelChangeEvent::ColumnsInserted: { + + ChildCache newCache; + ChildCache::ConstIterator iter = childToId.constBegin(); + + while (iter != childToId.constEnd()) { + QAccessible::Id id = iter.value(); + QAccessibleInterface *iface = QAccessible::accessibleInterface(id); + Q_ASSERT(iface); + if (indexOfChild(iface) >= 0) { + newCache.insert(indexOfChild(iface), id); + } else { + // ### This should really not happen, + // but it might if the view has a root index set. + // This needs to be fixed. + QAccessible::deleteAccessibleInterface(id); + } + ++iter; + } + childToId = newCache; + break; + } + + case QAccessibleTableModelChangeEvent::ColumnsRemoved: + case QAccessibleTableModelChangeEvent::RowsRemoved: { + ChildCache newCache; + ChildCache::ConstIterator iter = childToId.constBegin(); + while (iter != childToId.constEnd()) { + QAccessible::Id id = iter.value(); + QAccessibleInterface *iface = QAccessible::accessibleInterface(id); + Q_ASSERT(iface); + if (iface->role() == QAccessible::Cell || iface->role() == QAccessible::ListItem) { + Q_ASSERT(iface->tableCellInterface()); + AccessibleInstanceViewItem *cell = static_cast(iface->tableCellInterface()); + // Since it is a QPersistentModelIndex, we only need to check if it is valid + if (cell->m_index.isValid()) + newCache.insert(indexOfChild(cell), id); + else + QAccessible::deleteAccessibleInterface(id); + } + ++iter; + } + childToId = newCache; + break; + } + + case QAccessibleTableModelChangeEvent::DataChanged: + // nothing to do in this case + break; + } +} + +// TABLE CELL + +AccessibleInstanceViewItem::AccessibleInstanceViewItem(QAbstractItemView *view_, const QModelIndex &index_) + : view(view_), m_index(index_) +{ + if (Q_UNLIKELY(!index_.isValid())) + qWarning() << "AccessibleInstanceViewItem::AccessibleInstanceViewItem with invalid index: " << index_; +} + +void *AccessibleInstanceViewItem::interface_cast(QAccessible::InterfaceType t) +{ + if (t == QAccessible::TableCellInterface) + return static_cast(this); + if (t == QAccessible::ActionInterface) + return static_cast(this); + return 0; +} + +int AccessibleInstanceViewItem::columnExtent() const { return 1; } +int AccessibleInstanceViewItem::rowExtent() const { return 1; } + +QList AccessibleInstanceViewItem::rowHeaderCells() const +{ + return {}; +} + +QList AccessibleInstanceViewItem::columnHeaderCells() const +{ + return {}; +} + +int AccessibleInstanceViewItem::columnIndex() const +{ + if (!isValid()) { + return -1; + } + + return m_index.column(); +} + +int AccessibleInstanceViewItem::rowIndex() const +{ + if (!isValid()) { + return -1; + } + + return m_index.row(); +} + +bool AccessibleInstanceViewItem::isSelected() const +{ + if (!isValid()) { + return false; + } + + return view->selectionModel()->isSelected(m_index); +} + +QStringList AccessibleInstanceViewItem::actionNames() const +{ + QStringList names; + names << toggleAction(); + return names; +} + +void AccessibleInstanceViewItem::doAction(const QString& actionName) +{ + if (actionName == toggleAction()) { + if (isSelected()) { + unselectCell(); + } + else { + selectCell(); + } + } +} + +QStringList AccessibleInstanceViewItem::keyBindingsForAction(const QString &) const +{ + return QStringList(); +} + + +void AccessibleInstanceViewItem::selectCell() +{ + if (!isValid()) { + return; + } + QAbstractItemView::SelectionMode selectionMode = view->selectionMode(); + if (selectionMode == QAbstractItemView::NoSelection) { + return; + } + + Q_ASSERT(table()); + QAccessibleTableInterface *cellTable = table()->tableInterface(); + + switch (view->selectionBehavior()) { + case QAbstractItemView::SelectItems: + break; + case QAbstractItemView::SelectColumns: + if (cellTable) + cellTable->selectColumn(m_index.column()); + return; + case QAbstractItemView::SelectRows: + if (cellTable) + cellTable->selectRow(m_index.row()); + return; + } + + if (selectionMode == QAbstractItemView::SingleSelection) { + view->clearSelection(); + } + + view->selectionModel()->select(m_index, QItemSelectionModel::Select); +} + +void AccessibleInstanceViewItem::unselectCell() +{ + if (!isValid()) + return; + QAbstractItemView::SelectionMode selectionMode = view->selectionMode(); + if (selectionMode == QAbstractItemView::NoSelection) + return; + + QAccessibleTableInterface *cellTable = table()->tableInterface(); + + switch (view->selectionBehavior()) { + case QAbstractItemView::SelectItems: + break; + case QAbstractItemView::SelectColumns: + if (cellTable) + cellTable->unselectColumn(m_index.column()); + return; + case QAbstractItemView::SelectRows: + if (cellTable) + cellTable->unselectRow(m_index.row()); + return; + } + + //If the mode is not MultiSelection or ExtendedSelection and only + //one cell is selected it cannot be unselected by the user + if ((selectionMode != QAbstractItemView::MultiSelection) && (selectionMode != QAbstractItemView::ExtendedSelection) && (view->selectionModel()->selectedIndexes().count() <= 1)) + return; + + view->selectionModel()->select(m_index, QItemSelectionModel::Deselect); +} + +QAccessibleInterface *AccessibleInstanceViewItem::table() const +{ + return QAccessible::queryAccessibleInterface(view); +} + +QAccessible::Role AccessibleInstanceViewItem::role() const +{ + return QAccessible::ListItem; +} + +QAccessible::State AccessibleInstanceViewItem::state() const +{ + QAccessible::State st; + if (!isValid()) + return st; + + QRect globalRect = view->rect(); + globalRect.translate(view->mapToGlobal(QPoint(0,0))); + if (!globalRect.intersects(rect())) + st.invisible = true; + + if (view->selectionModel()->isSelected(m_index)) + st.selected = true; + if (view->selectionModel()->currentIndex() == m_index) + st.focused = true; + if (m_index.model()->data(m_index, Qt::CheckStateRole).toInt() == Qt::Checked) + st.checked = true; + + Qt::ItemFlags flags = m_index.flags(); + if (flags & Qt::ItemIsSelectable) { + st.selectable = true; + st.focusable = true; + if (view->selectionMode() == QAbstractItemView::MultiSelection) + st.multiSelectable = true; + if (view->selectionMode() == QAbstractItemView::ExtendedSelection) + st.extSelectable = true; + } + return st; +} + + +QRect AccessibleInstanceViewItem::rect() const +{ + QRect r; + if (!isValid()) + return r; + r = view->visualRect(m_index); + + if (!r.isNull()) { + r.translate(view->viewport()->mapTo(view, QPoint(0,0))); + r.translate(view->mapToGlobal(QPoint(0, 0))); + } + return r; +} + +QString AccessibleInstanceViewItem::text(QAccessible::Text t) const +{ + QString value; + if (!isValid()) + return value; + QAbstractItemModel *model = view->model(); + switch (t) { + case QAccessible::Name: + value = model->data(m_index, Qt::AccessibleTextRole).toString(); + if (value.isEmpty()) + value = model->data(m_index, Qt::DisplayRole).toString(); + break; + case QAccessible::Description: + value = model->data(m_index, Qt::AccessibleDescriptionRole).toString(); + break; + default: + break; + } + return value; +} + +void AccessibleInstanceViewItem::setText(QAccessible::Text /*t*/, const QString &text) +{ + if (!isValid() || !(m_index.flags() & Qt::ItemIsEditable)) + return; + view->model()->setData(m_index, text); +} + +bool AccessibleInstanceViewItem::isValid() const +{ + return view && view->model() && m_index.isValid(); +} + +QAccessibleInterface *AccessibleInstanceViewItem::parent() const +{ + return QAccessible::queryAccessibleInterface(view); +} + +QAccessibleInterface *AccessibleInstanceViewItem::child(int) const +{ + return 0; +} + +#endif /* !QT_NO_ACCESSIBILITY */ diff --git a/ultimmc/launcher/ui/instanceview/AccessibleInstanceView.h b/ultimmc/launcher/ui/instanceview/AccessibleInstanceView.h new file mode 100644 index 0000000..9bfd174 --- /dev/null +++ b/ultimmc/launcher/ui/instanceview/AccessibleInstanceView.h @@ -0,0 +1,6 @@ +#pragma once + +#include +class QAccessibleInterface; + +QAccessibleInterface *groupViewAccessibleFactory(const QString &classname, QObject *object); diff --git a/ultimmc/launcher/ui/instanceview/AccessibleInstanceView_p.h b/ultimmc/launcher/ui/instanceview/AccessibleInstanceView_p.h new file mode 100644 index 0000000..26462f5 --- /dev/null +++ b/ultimmc/launcher/ui/instanceview/AccessibleInstanceView_p.h @@ -0,0 +1,118 @@ +#pragma once + +#include "QtCore/qpointer.h" +#include +#include +#include +#ifndef QT_NO_ACCESSIBILITY +#include "InstanceView.h" +// #include + +class QAccessibleTableCell; +class QAccessibleTableHeaderCell; + +class AccessibleInstanceView :public QAccessibleTableInterface, public QAccessibleObject +{ +public: + explicit AccessibleInstanceView(QWidget *w); + bool isValid() const override; + + QAccessible::Role role() const override; + QAccessible::State state() const override; + QString text(QAccessible::Text t) const override; + QRect rect() const override; + + QAccessibleInterface *childAt(int x, int y) const override; + int childCount() const override; + int indexOfChild(const QAccessibleInterface *) const override; + + QAccessibleInterface *parent() const override; + QAccessibleInterface *child(int index) const override; + + void *interface_cast(QAccessible::InterfaceType t) override; + + // table interface + QAccessibleInterface *cellAt(int row, int column) const override; + QAccessibleInterface *caption() const override; + QAccessibleInterface *summary() const override; + QString columnDescription(int column) const override; + QString rowDescription(int row) const override; + int columnCount() const override; + int rowCount() const override; + + // selection + int selectedCellCount() const override; + int selectedColumnCount() const override; + int selectedRowCount() const override; + QList selectedCells() const override; + QList selectedColumns() const override; + QList selectedRows() const override; + bool isColumnSelected(int column) const override; + bool isRowSelected(int row) const override; + bool selectRow(int row) override; + bool selectColumn(int column) override; + bool unselectRow(int row) override; + bool unselectColumn(int column) override; + + QAbstractItemView *view() const; + + void modelChange(QAccessibleTableModelChangeEvent *event) override; + +protected: + // maybe vector + typedef QHash ChildCache; + mutable ChildCache childToId; + + virtual ~AccessibleInstanceView(); + +private: + inline int logicalIndex(const QModelIndex &index) const; +}; + +class AccessibleInstanceViewItem: public QAccessibleInterface, public QAccessibleTableCellInterface, public QAccessibleActionInterface +{ +public: + AccessibleInstanceViewItem(QAbstractItemView *view, const QModelIndex &m_index); + + void *interface_cast(QAccessible::InterfaceType t) override; + QObject *object() const override { return nullptr; } + QAccessible::Role role() const override; + QAccessible::State state() const override; + QRect rect() const override; + bool isValid() const override; + + QAccessibleInterface *childAt(int, int) const override { return nullptr; } + int childCount() const override { return 0; } + int indexOfChild(const QAccessibleInterface *) const override { return -1; } + + QString text(QAccessible::Text t) const override; + void setText(QAccessible::Text t, const QString &text) override; + + QAccessibleInterface *parent() const override; + QAccessibleInterface *child(int) const override; + + // cell interface + int columnExtent() const override; + QList columnHeaderCells() const override; + int columnIndex() const override; + int rowExtent() const override; + QList rowHeaderCells() const override; + int rowIndex() const override; + bool isSelected() const override; + QAccessibleInterface* table() const override; + + //action interface + QStringList actionNames() const override; + void doAction(const QString &actionName) override; + QStringList keyBindingsForAction(const QString &actionName) const override; + +private: + QPointer view; + QPersistentModelIndex m_index; + + void selectCell(); + void unselectCell(); + + friend class AccessibleInstanceView; +}; +#endif /* !QT_NO_ACCESSIBILITY */ diff --git a/ultimmc/launcher/ui/instanceview/InstanceDelegate.cpp b/ultimmc/launcher/ui/instanceview/InstanceDelegate.cpp new file mode 100644 index 0000000..3c4ca63 --- /dev/null +++ b/ultimmc/launcher/ui/instanceview/InstanceDelegate.cpp @@ -0,0 +1,428 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "InstanceDelegate.h" +#include +#include +#include +#include +#include +#include + +#include "InstanceView.h" +#include "BaseInstance.h" +#include "InstanceList.h" +#include +#include + +// Origin: Qt +static void viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height, + qreal &widthUsed) +{ + height = 0; + widthUsed = 0; + textLayout.beginLayout(); + QString str = textLayout.text(); + while (true) + { + QTextLine line = textLayout.createLine(); + if (!line.isValid()) + break; + if (line.textLength() == 0) + break; + line.setLineWidth(lineWidth); + line.setPosition(QPointF(0, height)); + height += line.height(); + widthUsed = qMax(widthUsed, line.naturalTextWidth()); + } + textLayout.endLayout(); +} + +ListViewDelegate::ListViewDelegate(QObject *parent) : QStyledItemDelegate(parent) +{ +} + +void drawSelectionRect(QPainter *painter, const QStyleOptionViewItem &option, + const QRect &rect) +{ + if ((option.state & QStyle::State_Selected)) + painter->fillRect(rect, option.palette.brush(QPalette::Highlight)); + else + { + QColor backgroundColor = option.palette.color(QPalette::Background); + backgroundColor.setAlpha(160); + painter->fillRect(rect, QBrush(backgroundColor)); + } +} + +void drawFocusRect(QPainter *painter, const QStyleOptionViewItem &option, const QRect &rect) +{ + if (!(option.state & QStyle::State_HasFocus)) + return; + QStyleOptionFocusRect opt; + opt.direction = option.direction; + opt.fontMetrics = option.fontMetrics; + opt.palette = option.palette; + opt.rect = rect; + // opt.state = option.state | QStyle::State_KeyboardFocusChange | + // QStyle::State_Item; + auto col = option.state & QStyle::State_Selected ? QPalette::Highlight : QPalette::Base; + opt.backgroundColor = option.palette.color(col); + // Apparently some widget styles expect this hint to not be set + painter->setRenderHint(QPainter::Antialiasing, false); + + QStyle *style = option.widget ? option.widget->style() : QApplication::style(); + + style->drawPrimitive(QStyle::PE_FrameFocusRect, &opt, painter, option.widget); + + painter->setRenderHint(QPainter::Antialiasing); +} + +// TODO this can be made a lot prettier +void drawProgressOverlay(QPainter *painter, const QStyleOptionViewItem &option, + const int value, const int maximum) +{ + if (maximum == 0 || value == maximum) + { + return; + } + + painter->save(); + + qreal percent = (qreal)value / (qreal)maximum; + QColor color = option.palette.color(QPalette::Dark); + color.setAlphaF(0.70f); + painter->setBrush(color); + painter->setPen(QPen(QBrush(), 0)); + painter->drawPie(option.rect, 90 * 16, -percent * 360 * 16); + + painter->restore(); +} + +void drawBadges(QPainter *painter, const QStyleOptionViewItem &option, BaseInstance *instance, QIcon::Mode mode, QIcon::State state) +{ + QList pixmaps; + if (instance->isRunning()) + { + pixmaps.append("status-running"); + } + else if (instance->hasCrashed() || instance->hasVersionBroken()) + { + pixmaps.append("status-bad"); + } + if (instance->hasUpdateAvailable()) + { + pixmaps.append("checkupdate"); + } + + static const int itemSide = 24; + static const int spacing = 1; + const int itemsPerRow = qMax(1, qFloor(double(option.rect.width() + spacing) / double(itemSide + spacing))); + const int rows = qCeil((double)pixmaps.size() / (double)itemsPerRow); + QListIterator it(pixmaps); + painter->translate(option.rect.topLeft()); + for (int y = 0; y < rows; ++y) + { + for (int x = 0; x < itemsPerRow; ++x) + { + if (!it.hasNext()) + { + return; + } + // FIXME: inject this. + auto icon = XdgIcon::fromTheme(it.next()); + // opt.icon.paint(painter, iconbox, Qt::AlignCenter, mode, state); + const QPixmap pixmap; + // itemSide + QRect badgeRect( + option.rect.width() - x * itemSide + qMax(x - 1, 0) * spacing - itemSide, + y * itemSide + qMax(y - 1, 0) * spacing, + itemSide, + itemSide + ); + icon.paint(painter, badgeRect, Qt::AlignCenter, mode, state); + } + } + painter->translate(-option.rect.topLeft()); +} + +static QSize viewItemTextSize(const QStyleOptionViewItem *option) +{ + QStyle *style = option->widget ? option->widget->style() : QApplication::style(); + QTextOption textOption; + textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + QTextLayout textLayout; + textLayout.setTextOption(textOption); + textLayout.setFont(option->font); + textLayout.setText(option->text); + const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, option, option->widget) + 1; + QRect bounds(0, 0, 100 - 2 * textMargin, 600); + qreal height = 0, widthUsed = 0; + viewItemTextLayout(textLayout, bounds.width(), height, widthUsed); + const QSize size(qCeil(widthUsed), qCeil(height)); + return QSize(size.width() + 2 * textMargin, size.height()); +} + +void ListViewDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + painter->save(); + painter->setClipRect(opt.rect); + + opt.features |= QStyleOptionViewItem::WrapText; + opt.text = index.data().toString(); + opt.textElideMode = Qt::ElideRight; + opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter; + + QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); + + // const int iconSize = style->pixelMetric(QStyle::PM_IconViewIconSize); + const int iconSize = 48; + QRect iconbox = opt.rect; + const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, 0, opt.widget) + 1; + QRect textRect = opt.rect; + QRect textHighlightRect = textRect; + // clip the decoration on top, remove width padding + textRect.adjust(textMargin, iconSize + textMargin + 5, -textMargin, 0); + + textHighlightRect.adjust(0, iconSize + 5, 0, 0); + + // draw background + { + // FIXME: unused + // QSize textSize = viewItemTextSize ( &opt ); + drawSelectionRect(painter, opt, textHighlightRect); + /* + QPalette::ColorGroup cg; + QStyleOptionViewItem opt2(opt); + + if ((opt.widget && opt.widget->isEnabled()) || (opt.state & QStyle::State_Enabled)) + { + if (!(opt.state & QStyle::State_Active)) + cg = QPalette::Inactive; + else + cg = QPalette::Normal; + } + else + { + cg = QPalette::Disabled; + } + */ + /* + opt2.palette.setCurrentColorGroup(cg); + + // fill in background, if any + + + if (opt.backgroundBrush.style() != Qt::NoBrush) + { + QPointF oldBO = painter->brushOrigin(); + painter->setBrushOrigin(opt.rect.topLeft()); + painter->fillRect(opt.rect, opt.backgroundBrush); + painter->setBrushOrigin(oldBO); + } + + drawSelectionRect(painter, opt2, textHighlightRect); + */ + + /* + if (opt.showDecorationSelected) + { + drawSelectionRect(painter, opt2, opt.rect); + drawFocusRect(painter, opt2, opt.rect); + // painter->fillRect ( opt.rect, opt.palette.brush ( cg, QPalette::Highlight ) ); + } + else + { + + // if ( opt.state & QStyle::State_Selected ) + { + // QRect textRect = subElementRect ( QStyle::SE_ItemViewItemText, opt, + // opt.widget ); + // painter->fillRect ( textHighlightRect, opt.palette.brush ( cg, + // QPalette::Highlight ) ); + drawSelectionRect(painter, opt2, textHighlightRect); + drawFocusRect(painter, opt2, textHighlightRect); + } + } + */ + } + + // icon mode and state, also used for badges + QIcon::Mode mode = QIcon::Normal; + if (!(opt.state & QStyle::State_Enabled)) + mode = QIcon::Disabled; + else if (opt.state & QStyle::State_Selected) + mode = QIcon::Selected; + QIcon::State state = opt.state & QStyle::State_Open ? QIcon::On : QIcon::Off; + + // draw the icon + { + iconbox.setHeight(iconSize); + opt.icon.paint(painter, iconbox, Qt::AlignCenter, mode, state); + } + // set the text colors + QPalette::ColorGroup cg = + opt.state & QStyle::State_Enabled ? QPalette::Normal : QPalette::Disabled; + if (cg == QPalette::Normal && !(opt.state & QStyle::State_Active)) + cg = QPalette::Inactive; + if (opt.state & QStyle::State_Selected) + { + painter->setPen(opt.palette.color(cg, QPalette::HighlightedText)); + } + else + { + painter->setPen(opt.palette.color(cg, QPalette::Text)); + } + + // draw the text + QTextOption textOption; + textOption.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + textOption.setTextDirection(opt.direction); + textOption.setAlignment(QStyle::visualAlignment(opt.direction, opt.displayAlignment)); + QTextLayout textLayout; + textLayout.setTextOption(textOption); + textLayout.setFont(opt.font); + textLayout.setText(opt.text); + + qreal width, height; + viewItemTextLayout(textLayout, textRect.width(), height, width); + + const int lineCount = textLayout.lineCount(); + + const QRect layoutRect = QStyle::alignedRect( + opt.direction, opt.displayAlignment, QSize(textRect.width(), int(height)), textRect); + const QPointF position = layoutRect.topLeft(); + for (int i = 0; i < lineCount; ++i) + { + const QTextLine line = textLayout.lineAt(i); + line.draw(painter, position); + } + + // FIXME: this really has no business of being here. Make generic. + auto instance = (BaseInstance*)index.data(InstanceList::InstancePointerRole) + .value(); + if (instance) + { + drawBadges(painter, opt, instance, mode, state); + } + + drawProgressOverlay(painter, opt, index.data(InstanceViewRoles::ProgressValueRole).toInt(), + index.data(InstanceViewRoles::ProgressMaximumRole).toInt()); + + painter->restore(); +} + +QSize ListViewDelegate::sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + QStyleOptionViewItem opt = option; + initStyleOption(&opt, index); + opt.features |= QStyleOptionViewItem::WrapText; + opt.text = index.data().toString(); + opt.textElideMode = Qt::ElideRight; + opt.displayAlignment = Qt::AlignTop | Qt::AlignHCenter; + + QStyle *style = opt.widget ? opt.widget->style() : QApplication::style(); + const int textMargin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &option, opt.widget) + 1; + int height = 48 + textMargin * 2 + 5; // TODO: turn constants into variables + QSize szz = viewItemTextSize(&opt); + height += szz.height(); + // FIXME: maybe the icon items could scale and keep proportions? + QSize sz(100, height); + return sz; +} + +class NoReturnTextEdit: public QTextEdit +{ + Q_OBJECT +public: + explicit NoReturnTextEdit(QWidget *parent) : QTextEdit(parent) + { + setTextInteractionFlags(Qt::TextEditorInteraction); + setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOff); + } + bool event(QEvent * event) override + { + auto eventType = event->type(); + if(eventType == QEvent::KeyPress || eventType == QEvent::KeyRelease) + { + QKeyEvent *keyEvent = static_cast(event); + auto key = keyEvent->key(); + if (key == Qt::Key_Return || key == Qt::Key_Enter) + { + emit editingDone(); + return true; + } + if(key == Qt::Key_Tab) + { + return true; + } + } + return QTextEdit::event(event); + } +signals: + void editingDone(); +}; + +void ListViewDelegate::updateEditorGeometry(QWidget* editor, const QStyleOptionViewItem& option, const QModelIndex& index) const +{ + const int iconSize = 48; + QRect textRect = option.rect; + // QStyle *style = option.widget ? option.widget->style() : QApplication::style(); + textRect.adjust(0, iconSize + 5, 0, 0); + editor->setGeometry(textRect); +} + +void ListViewDelegate::setEditorData(QWidget* editor, const QModelIndex& index) const +{ + auto text = index.data(Qt::EditRole).toString(); + QTextEdit * realeditor = qobject_cast(editor); + realeditor->setAlignment(Qt::AlignHCenter | Qt::AlignTop); + realeditor->append(text); + realeditor->selectAll(); + realeditor->document()->clearUndoRedoStacks(); +} + +void ListViewDelegate::setModelData(QWidget* editor, QAbstractItemModel* model, const QModelIndex& index) const +{ + QTextEdit * realeditor = qobject_cast(editor); + QString text = realeditor->toPlainText(); + text.replace(QChar('\n'), QChar(' ')); + text = text.trimmed(); + if(text.size() != 0) + { + model->setData(index, text); + } +} + +QWidget * ListViewDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& option, const QModelIndex& index) const +{ + auto editor = new NoReturnTextEdit(parent); + connect(editor, &NoReturnTextEdit::editingDone, this, &ListViewDelegate::editingDone); + return editor; +} + +void ListViewDelegate::editingDone() +{ + NoReturnTextEdit *editor = qobject_cast(sender()); + emit commitData(editor); + emit closeEditor(editor); +} + +#include "InstanceDelegate.moc" diff --git a/ultimmc/launcher/ui/instanceview/InstanceDelegate.h b/ultimmc/launcher/ui/instanceview/InstanceDelegate.h new file mode 100644 index 0000000..d95279f --- /dev/null +++ b/ultimmc/launcher/ui/instanceview/InstanceDelegate.h @@ -0,0 +1,39 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +class ListViewDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + explicit ListViewDelegate(QObject *parent = 0); + virtual ~ListViewDelegate() {} + + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void updateEditorGeometry(QWidget * editor, const QStyleOptionViewItem & option, const QModelIndex & index) const override; + QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override; + + void setEditorData(QWidget * editor, const QModelIndex & index) const override; + void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override; + +private slots: + void editingDone(); +}; diff --git a/ultimmc/launcher/ui/instanceview/InstanceProxyModel.cpp b/ultimmc/launcher/ui/instanceview/InstanceProxyModel.cpp new file mode 100644 index 0000000..d8de93e --- /dev/null +++ b/ultimmc/launcher/ui/instanceview/InstanceProxyModel.cpp @@ -0,0 +1,71 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "InstanceProxyModel.h" + +#include "InstanceView.h" +#include "Application.h" +#include +#include + +#include + +InstanceProxyModel::InstanceProxyModel(QObject *parent) : QSortFilterProxyModel(parent) { + m_naturalSort.setNumericMode(true); + m_naturalSort.setCaseSensitivity(Qt::CaseSensitivity::CaseInsensitive); + // FIXME: use loaded translation as source of locale instead, hook this up to translation changes + m_naturalSort.setLocale(QLocale::system()); +} + +QVariant InstanceProxyModel::data(const QModelIndex & index, int role) const +{ + QVariant data = QSortFilterProxyModel::data(index, role); + if(role == Qt::DecorationRole) + { + return QVariant(APPLICATION->icons()->getIcon(data.toString())); + } + return data; +} + +bool InstanceProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const { + const QString leftCategory = left.data(InstanceViewRoles::GroupRole).toString(); + const QString rightCategory = right.data(InstanceViewRoles::GroupRole).toString(); + if (leftCategory == rightCategory) { + return subSortLessThan(left, right); + } + else { + // FIXME: real group sorting happens in InstanceView::updateGeometries(), see LocaleString + auto result = leftCategory.localeAwareCompare(rightCategory); + if(result == 0) { + return subSortLessThan(left, right); + } + return result < 0; + } +} + +bool InstanceProxyModel::subSortLessThan(const QModelIndex &left, const QModelIndex &right) const +{ + BaseInstance *pdataLeft = static_cast(left.internalPointer()); + BaseInstance *pdataRight = static_cast(right.internalPointer()); + QString sortMode = APPLICATION->settings()->get("InstSortMode").toString(); + if (sortMode == "LastLaunch") + { + return pdataLeft->lastLaunch() > pdataRight->lastLaunch(); + } + else + { + return m_naturalSort.compare(pdataLeft->name(), pdataRight->name()) < 0; + } +} diff --git a/ultimmc/launcher/ui/instanceview/InstanceProxyModel.h b/ultimmc/launcher/ui/instanceview/InstanceProxyModel.h new file mode 100644 index 0000000..bba8d2b --- /dev/null +++ b/ultimmc/launcher/ui/instanceview/InstanceProxyModel.h @@ -0,0 +1,35 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +class InstanceProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + InstanceProxyModel(QObject *parent = 0); + +protected: + QVariant data(const QModelIndex & index, int role) const override; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + bool subSortLessThan(const QModelIndex &left, const QModelIndex &right) const; + +private: + QCollator m_naturalSort; +}; diff --git a/ultimmc/launcher/ui/instanceview/InstanceView.cpp b/ultimmc/launcher/ui/instanceview/InstanceView.cpp new file mode 100644 index 0000000..25aec1a --- /dev/null +++ b/ultimmc/launcher/ui/instanceview/InstanceView.cpp @@ -0,0 +1,1001 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "InstanceView.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "VisualGroup.h" +#include + +#include +#include + + +template bool listsIntersect(const QList &l1, const QList t2) +{ + for (auto &item : l1) + { + if (t2.contains(item)) + { + return true; + } + } + return false; +} + +InstanceView::InstanceView(QWidget *parent) + : QAbstractItemView(parent) +{ + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + setAcceptDrops(true); + setAutoScroll(true); +} + +InstanceView::~InstanceView() +{ + qDeleteAll(m_groups); + m_groups.clear(); +} + +void InstanceView::setModel(QAbstractItemModel *model) +{ + QAbstractItemView::setModel(model); + connect(model, &QAbstractItemModel::modelReset, this, &InstanceView::modelReset); + connect(model, &QAbstractItemModel::rowsRemoved, this, &InstanceView::rowsRemoved); +} + +void InstanceView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) +{ + scheduleDelayedItemsLayout(); +} +void InstanceView::rowsInserted(const QModelIndex &parent, int start, int end) +{ + scheduleDelayedItemsLayout(); +} + +void InstanceView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) +{ + scheduleDelayedItemsLayout(); +} + +void InstanceView::modelReset() +{ + scheduleDelayedItemsLayout(); +} + +void InstanceView::rowsRemoved() +{ + scheduleDelayedItemsLayout(); +} + +void InstanceView::currentChanged(const QModelIndex& current, const QModelIndex& previous) +{ + QAbstractItemView::currentChanged(current, previous); + // TODO: for accessibility support, implement+register a factory, steal QAccessibleTable from Qt and return an instance of it for InstanceView. +#ifndef QT_NO_ACCESSIBILITY + if (QAccessible::isActive() && current.isValid()) { + QAccessibleEvent event(this, QAccessible::Focus); + event.setChild(current.row()); + QAccessible::updateAccessibility(&event); + } +#endif /* !QT_NO_ACCESSIBILITY */ +} + + +class LocaleString : public QString +{ +public: + LocaleString(const char *s) : QString(s) + { + } + LocaleString(const QString &s) : QString(s) + { + } +}; + +inline bool operator<(const LocaleString &lhs, const LocaleString &rhs) +{ + return (QString::localeAwareCompare(lhs, rhs) < 0); +} + +void InstanceView::updateScrollbar() +{ + int previousScroll = verticalScrollBar()->value(); + if (m_groups.isEmpty()) + { + verticalScrollBar()->setRange(0, 0); + } + else + { + int totalHeight = 0; + // top margin + totalHeight += m_categoryMargin; + int itemScroll = 0; + for (auto category : m_groups) + { + category->m_verticalPosition = totalHeight; + totalHeight += category->totalHeight() + m_categoryMargin; + if(!itemScroll && category->totalHeight() != 0) + { + itemScroll = category->contentHeight() / category->numRows(); + } + } + // do not divide by zero + if(itemScroll == 0) + itemScroll = 64; + + totalHeight += m_bottomMargin; + verticalScrollBar()->setSingleStep ( itemScroll ); + const int rowsPerPage = qMax ( viewport()->height() / itemScroll, 1 ); + verticalScrollBar()->setPageStep ( rowsPerPage * itemScroll ); + + verticalScrollBar()->setRange(0, totalHeight - height()); + } + + verticalScrollBar()->setValue(qMin(previousScroll, verticalScrollBar()->maximum())); +} + +void InstanceView::updateGeometries() +{ + geometryCache.clear(); + + QMap cats; + + for (int i = 0; i < model()->rowCount(); ++i) + { + const QString groupName = model()->index(i, 0).data(InstanceViewRoles::GroupRole).toString(); + if (!cats.contains(groupName)) + { + VisualGroup *old = this->category(groupName); + if (old) + { + auto cat = new VisualGroup(old); + cats.insert(groupName, cat); + cat->update(); + } + else + { + auto cat = new VisualGroup(groupName, this); + if(fVisibility) { + cat->collapsed = fVisibility(groupName); + } + cats.insert(groupName, cat); + cat->update(); + } + } + } + + qDeleteAll(m_groups); + m_groups = cats.values(); + updateScrollbar(); + viewport()->update(); +} + +bool InstanceView::isIndexHidden(const QModelIndex &index) const +{ + VisualGroup *cat = category(index); + if (cat) + { + return cat->collapsed; + } + else + { + return false; + } +} + +VisualGroup *InstanceView::category(const QModelIndex &index) const +{ + return category(index.data(InstanceViewRoles::GroupRole).toString()); +} + +VisualGroup *InstanceView::category(const QString &cat) const +{ + for (auto group : m_groups) + { + if (group->text == cat) + { + return group; + } + } + return nullptr; +} + +VisualGroup *InstanceView::categoryAt(const QPoint &pos, VisualGroup::HitResults & result) const +{ + for (auto group : m_groups) + { + result = group->hitScan(pos); + if(result != VisualGroup::NoHit) + { + return group; + } + } + result = VisualGroup::NoHit; + return nullptr; +} + +QString InstanceView::groupNameAt(const QPoint &point) +{ + executeDelayedItemsLayout(); + + VisualGroup::HitResults hitresult; + auto group = categoryAt(point + offset(), hitresult); + if(group && (hitresult & (VisualGroup::HeaderHit | VisualGroup::BodyHit))) + { + return group->text; + } + return QString(); +} + +int InstanceView::calculateItemsPerRow() const +{ + return qFloor((qreal)(contentWidth()) / (qreal)(itemWidth() + m_spacing)); +} + +int InstanceView::contentWidth() const +{ + return width() - m_leftMargin - m_rightMargin; +} + +int InstanceView::itemWidth() const +{ + return m_itemWidth; +} + +void InstanceView::mousePressEvent(QMouseEvent *event) +{ + executeDelayedItemsLayout(); + + QPoint visualPos = event->pos(); + QPoint geometryPos = event->pos() + offset(); + + QPersistentModelIndex index = indexAt(visualPos); + + m_pressedIndex = index; + m_pressedAlreadySelected = selectionModel()->isSelected(m_pressedIndex); + m_pressedPosition = geometryPos; + + VisualGroup::HitResults hitresult; + m_pressedCategory = categoryAt(geometryPos, hitresult); + if (m_pressedCategory && hitresult & VisualGroup::CheckboxHit) + { + setState(m_pressedCategory->collapsed ? ExpandingState : CollapsingState); + event->accept(); + return; + } + + if (index.isValid() && (index.flags() & Qt::ItemIsEnabled)) + { + if(index != currentIndex()) + { + // FIXME: better! + m_currentCursorColumn = -1; + } + // we disable scrollTo for mouse press so the item doesn't change position + // when the user is interacting with it (ie. clicking on it) + bool autoScroll = hasAutoScroll(); + setAutoScroll(false); + selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); + + setAutoScroll(autoScroll); + QRect rect(visualPos, visualPos); + setSelection(rect, QItemSelectionModel::ClearAndSelect); + + // signal handlers may change the model + emit pressed(index); + } + else + { + // Forces a finalize() even if mouse is pressed, but not on a item + selectionModel()->select(QModelIndex(), QItemSelectionModel::Select); + } +} + +void InstanceView::mouseMoveEvent(QMouseEvent *event) +{ + executeDelayedItemsLayout(); + + QPoint topLeft; + QPoint visualPos = event->pos(); + QPoint geometryPos = event->pos() + offset(); + + if (state() == ExpandingState || state() == CollapsingState) + { + return; + } + + if (state() == DraggingState) + { + topLeft = m_pressedPosition - offset(); + if ((topLeft - event->pos()).manhattanLength() > QApplication::startDragDistance()) + { + m_pressedIndex = QModelIndex(); + startDrag(model()->supportedDragActions()); + setState(NoState); + stopAutoScroll(); + } + return; + } + + if (selectionMode() != SingleSelection) + { + topLeft = m_pressedPosition - offset(); + } + else + { + topLeft = geometryPos; + } + + if (m_pressedIndex.isValid() && (state() != DragSelectingState) && + (event->buttons() != Qt::NoButton) && !selectedIndexes().isEmpty()) + { + setState(DraggingState); + return; + } + + if ((event->buttons() & Qt::LeftButton) && selectionModel()) + { + setState(DragSelectingState); + + setSelection(QRect(visualPos, visualPos), QItemSelectionModel::ClearAndSelect); + QModelIndex index = indexAt(visualPos); + + // set at the end because it might scroll the view + if (index.isValid() && (index != selectionModel()->currentIndex()) && + (index.flags() & Qt::ItemIsEnabled)) + { + selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); + } + } +} + +void InstanceView::mouseReleaseEvent(QMouseEvent *event) +{ + executeDelayedItemsLayout(); + + QPoint visualPos = event->pos(); + QPoint geometryPos = event->pos() + offset(); + QPersistentModelIndex index = indexAt(visualPos); + + VisualGroup::HitResults hitresult; + + bool click = (index == m_pressedIndex && index.isValid()) || + (m_pressedCategory && m_pressedCategory == categoryAt(geometryPos, hitresult)); + + if (click && m_pressedCategory) + { + if (state() == ExpandingState) + { + m_pressedCategory->collapsed = false; + emit groupStateChanged(m_pressedCategory->text, false); + + updateGeometries(); + viewport()->update(); + event->accept(); + m_pressedCategory = nullptr; + setState(NoState); + return; + } + else if (state() == CollapsingState) + { + m_pressedCategory->collapsed = true; + emit groupStateChanged(m_pressedCategory->text, true); + + updateGeometries(); + viewport()->update(); + event->accept(); + m_pressedCategory = nullptr; + setState(NoState); + return; + } + } + + m_ctrlDragSelectionFlag = QItemSelectionModel::NoUpdate; + + setState(NoState); + + if (click) + { + if (event->button() == Qt::LeftButton) + { + emit clicked(index); + } + QStyleOptionViewItem option = viewOptions(); + if (m_pressedAlreadySelected) + { + option.state |= QStyle::State_Selected; + } + if ((model()->flags(index) & Qt::ItemIsEnabled) && + style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this)) + { + emit activated(index); + } + } +} + +void InstanceView::mouseDoubleClickEvent(QMouseEvent *event) +{ + executeDelayedItemsLayout(); + + QModelIndex index = indexAt(event->pos()); + if (!index.isValid() || !(index.flags() & Qt::ItemIsEnabled) || (m_pressedIndex != index)) + { + QMouseEvent me( + QEvent::MouseButtonPress, + event->localPos(), + event->windowPos(), + event->screenPos(), + event->button(), + event->buttons(), + event->modifiers() + ); + mousePressEvent(&me); + return; + } + // signal handlers may change the model + QPersistentModelIndex persistent = index; + emit doubleClicked(persistent); + + QStyleOptionViewItem option = viewOptions(); + if ((model()->flags(index) & Qt::ItemIsEnabled) && !style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, &option, this)) + { + emit activated(index); + } +} + +void InstanceView::paintEvent(QPaintEvent *event) +{ + executeDelayedItemsLayout(); + + QPainter painter(this->viewport()); + + QStyleOptionViewItem option(viewOptions()); + option.widget = this; + + int wpWidth = viewport()->width(); + option.rect.setWidth(wpWidth); + for (int i = 0; i < m_groups.size(); ++i) + { + VisualGroup *category = m_groups.at(i); + int y = category->verticalPosition(); + y -= verticalOffset(); + QRect backup = option.rect; + int height = category->totalHeight(); + option.rect.setTop(y); + option.rect.setHeight(height); + option.rect.setLeft(m_leftMargin); + option.rect.setRight(wpWidth - m_rightMargin); + category->drawHeader(&painter, option); + y += category->totalHeight() + m_categoryMargin; + option.rect = backup; + } + + for (int i = 0; i < model()->rowCount(); ++i) + { + const QModelIndex index = model()->index(i, 0); + if (isIndexHidden(index)) + { + continue; + } + Qt::ItemFlags flags = index.flags(); + option.rect = visualRect(index); + option.features |= QStyleOptionViewItem::WrapText; + if (flags & Qt::ItemIsSelectable && selectionModel()->isSelected(index)) + { + option.state |= selectionModel()->isSelected(index) ? QStyle::State_Selected + : QStyle::State_None; + } + else + { + option.state &= ~QStyle::State_Selected; + } + option.state |= (index == currentIndex()) ? QStyle::State_HasFocus : QStyle::State_None; + if (!(flags & Qt::ItemIsEnabled)) + { + option.state &= ~QStyle::State_Enabled; + } + itemDelegate()->paint(&painter, option, index); + } + + /* + * Drop indicators for manual reordering... + */ +#if 0 + if (!m_lastDragPosition.isNull()) + { + QPair pair = rowDropPos(m_lastDragPosition); + Group *category = pair.first; + int row = pair.second; + if (category) + { + int internalRow = row - category->firstItemIndex; + QLine line; + if (internalRow >= category->numItems()) + { + QRect toTheRightOfRect = visualRect(category->lastItem()); + line = QLine(toTheRightOfRect.topRight(), toTheRightOfRect.bottomRight()); + } + else + { + QRect toTheLeftOfRect = visualRect(model()->index(row, 0)); + line = QLine(toTheLeftOfRect.topLeft(), toTheLeftOfRect.bottomLeft()); + } + painter.save(); + painter.setPen(QPen(Qt::black, 3)); + painter.drawLine(line); + painter.restore(); + } + } +#endif +} + +void InstanceView::resizeEvent(QResizeEvent *event) +{ + int newItemsPerRow = calculateItemsPerRow(); + if(newItemsPerRow != m_currentItemsPerRow) + { + m_currentCursorColumn = -1; + m_currentItemsPerRow = newItemsPerRow; + updateGeometries(); + } + else + { + updateScrollbar(); + } +} + +void InstanceView::dragEnterEvent(QDragEnterEvent *event) +{ + executeDelayedItemsLayout(); + + if (!isDragEventAccepted(event)) + { + return; + } + m_lastDragPosition = event->pos() + offset(); + viewport()->update(); + event->accept(); +} + +void InstanceView::dragMoveEvent(QDragMoveEvent *event) +{ + executeDelayedItemsLayout(); + + if (!isDragEventAccepted(event)) + { + return; + } + m_lastDragPosition = event->pos() + offset(); + viewport()->update(); + event->accept(); +} + +void InstanceView::dragLeaveEvent(QDragLeaveEvent *event) +{ + executeDelayedItemsLayout(); + + m_lastDragPosition = QPoint(); + viewport()->update(); +} + +void InstanceView::dropEvent(QDropEvent *event) +{ + executeDelayedItemsLayout(); + + m_lastDragPosition = QPoint(); + + stopAutoScroll(); + setState(NoState); + + auto mimedata = event->mimeData(); + + if (event->source() == this) + { + if(event->possibleActions() & Qt::MoveAction) + { + QPair dropPos = rowDropPos(event->pos()); + const VisualGroup *group = dropPos.first; + auto hitresult = dropPos.second; + + if (hitresult == VisualGroup::HitResult::NoHit) + { + viewport()->update(); + return; + } + auto instanceId = QString::fromUtf8(mimedata->data("application/x-instanceid")); + auto instanceList = APPLICATION->instances().get(); + instanceList->setInstanceGroup(instanceId, group->text); + event->setDropAction(Qt::MoveAction); + event->accept(); + + updateGeometries(); + viewport()->update(); + } + return; + } + + // check if the action is supported + if (!mimedata) + { + return; + } + + // files dropped from outside? + if (mimedata->hasUrls()) + { + auto urls = mimedata->urls(); + event->accept(); + emit droppedURLs(urls); + } +} + +void InstanceView::startDrag(Qt::DropActions supportedActions) +{ + executeDelayedItemsLayout(); + + QModelIndexList indexes = selectionModel()->selectedIndexes(); + if(indexes.count() == 0) + return; + + QMimeData *data = model()->mimeData(indexes); + if (!data) + { + return; + } + QRect rect; + QPixmap pixmap = renderToPixmap(indexes, &rect); + QDrag *drag = new QDrag(this); + drag->setPixmap(pixmap); + drag->setMimeData(data); + drag->setHotSpot(m_pressedPosition - rect.topLeft()); + Qt::DropAction defaultDropAction = Qt::IgnoreAction; + if (this->defaultDropAction() != Qt::IgnoreAction && (supportedActions & this->defaultDropAction())) + { + defaultDropAction = this->defaultDropAction(); + } + /*auto action = */ + drag->exec(supportedActions, defaultDropAction); +} + +QRect InstanceView::visualRect(const QModelIndex &index) const +{ + const_cast(this)->executeDelayedItemsLayout(); + + return geometryRect(index).translated(-offset()); +} + +QRect InstanceView::geometryRect(const QModelIndex &index) const +{ + const_cast(this)->executeDelayedItemsLayout(); + + if (!index.isValid() || isIndexHidden(index) || index.column() > 0) + { + return QRect(); + } + + int row = index.row(); + if(geometryCache.contains(row)) + { + return *geometryCache[row]; + } + + const VisualGroup *cat = category(index); + QPair pos = cat->positionOf(index); + int x = pos.first; + // int y = pos.second; + + QRect out; + out.setTop(cat->verticalPosition() + cat->headerHeight() + 5 + cat->rowTopOf(index)); + out.setLeft(m_spacing + x * (itemWidth() + m_spacing)); + out.setSize(itemDelegate()->sizeHint(viewOptions(), index)); + geometryCache.insert(row, new QRect(out)); + return out; +} + +QModelIndex InstanceView::indexAt(const QPoint &point) const +{ + const_cast(this)->executeDelayedItemsLayout(); + + for (int i = 0; i < model()->rowCount(); ++i) + { + QModelIndex index = model()->index(i, 0); + if (visualRect(index).contains(point)) + { + return index; + } + } + return QModelIndex(); +} + +void InstanceView::setSelection(const QRect &rect, const QItemSelectionModel::SelectionFlags commands) +{ + executeDelayedItemsLayout(); + + for (int i = 0; i < model()->rowCount(); ++i) + { + QModelIndex index = model()->index(i, 0); + QRect itemRect = visualRect(index); + if (itemRect.intersects(rect)) + { + selectionModel()->select(index, commands); + update(itemRect.translated(-offset())); + } + } +} + +QPixmap InstanceView::renderToPixmap(const QModelIndexList &indices, QRect *r) const +{ + Q_ASSERT(r); + auto paintPairs = draggablePaintPairs(indices, r); + if (paintPairs.isEmpty()) + { + return QPixmap(); + } + QPixmap pixmap(r->size()); + pixmap.fill(Qt::transparent); + QPainter painter(&pixmap); + QStyleOptionViewItem option = viewOptions(); + option.state |= QStyle::State_Selected; + for (int j = 0; j < paintPairs.count(); ++j) + { + option.rect = paintPairs.at(j).first.translated(-r->topLeft()); + const QModelIndex ¤t = paintPairs.at(j).second; + itemDelegate()->paint(&painter, option, current); + } + return pixmap; +} + +QList> InstanceView::draggablePaintPairs(const QModelIndexList &indices, QRect *r) const +{ + Q_ASSERT(r); + QRect &rect = *r; + QList> ret; + for (int i = 0; i < indices.count(); ++i) + { + const QModelIndex &index = indices.at(i); + const QRect current = geometryRect(index); + ret += qMakePair(current, index); + rect |= current; + } + return ret; +} + +bool InstanceView::isDragEventAccepted(QDropEvent *event) +{ + return true; +} + +QPair InstanceView::rowDropPos(const QPoint &pos) +{ + VisualGroup::HitResults hitresult; + auto group = categoryAt(pos + offset(), hitresult); + return qMakePair(group, hitresult); +} + +QPoint InstanceView::offset() const +{ + return QPoint(horizontalOffset(), verticalOffset()); +} + +QRegion InstanceView::visualRegionForSelection(const QItemSelection &selection) const +{ + QRegion region; + for (auto &range : selection) + { + int start_row = range.top(); + int end_row = range.bottom(); + for (int row = start_row; row <= end_row; ++row) + { + int start_column = range.left(); + int end_column = range.right(); + for (int column = start_column; column <= end_column; ++column) + { + QModelIndex index = model()->index(row, column, rootIndex()); + region += visualRect(index); // OK + } + } + } + return region; +} + +QModelIndex InstanceView::moveCursor(QAbstractItemView::CursorAction cursorAction, Qt::KeyboardModifiers modifiers) +{ + auto current = currentIndex(); + if(!current.isValid()) + { + return current; + } + auto cat = category(current); + int group_index = m_groups.indexOf(cat); + if(group_index < 0) + return current; + + QPair pos = cat->positionOf(current); + int column = pos.first; + int row = pos.second; + if(m_currentCursorColumn < 0) + { + m_currentCursorColumn = column; + } + switch(cursorAction) + { + case MoveUp: + { + if(row == 0) + { + int prevgroupindex = group_index-1; + while(prevgroupindex >= 0) + { + auto prevgroup = m_groups[prevgroupindex]; + if(prevgroup->collapsed) + { + prevgroupindex--; + continue; + } + int newRow = prevgroup->numRows() - 1; + int newRowSize = prevgroup->rows[newRow].size(); + int newColumn = m_currentCursorColumn; + if (m_currentCursorColumn >= newRowSize) + { + newColumn = newRowSize - 1; + } + return prevgroup->rows[newRow][newColumn]; + } + } + else + { + int newRow = row - 1; + int newRowSize = cat->rows[newRow].size(); + int newColumn = m_currentCursorColumn; + if (m_currentCursorColumn >= newRowSize) + { + newColumn = newRowSize - 1; + } + return cat->rows[newRow][newColumn]; + } + return current; + } + case MoveDown: + { + if(row == cat->rows.size() - 1) + { + int nextgroupindex = group_index+1; + while (nextgroupindex < m_groups.size()) + { + auto nextgroup = m_groups[nextgroupindex]; + if(nextgroup->collapsed) + { + nextgroupindex++; + continue; + } + int newRowSize = nextgroup->rows[0].size(); + int newColumn = m_currentCursorColumn; + if (m_currentCursorColumn >= newRowSize) + { + newColumn = newRowSize - 1; + } + return nextgroup->rows[0][newColumn]; + } + } + else + { + int newRow = row + 1; + int newRowSize = cat->rows[newRow].size(); + int newColumn = m_currentCursorColumn; + if (m_currentCursorColumn >= newRowSize) + { + newColumn = newRowSize - 1; + } + return cat->rows[newRow][newColumn]; + } + return current; + } + case MoveLeft: + { + if(column > 0) + { + m_currentCursorColumn = column - 1; + return cat->rows[row][column - 1]; + } + // TODO: moving to previous line + return current; + } + case MoveRight: + { + if(column < cat->rows[row].size() - 1) + { + m_currentCursorColumn = column + 1; + return cat->rows[row][column + 1]; + } + // TODO: moving to next line + return current; + } + case MoveHome: + { + m_currentCursorColumn = 0; + return cat->rows[row][0]; + } + case MoveEnd: + { + auto last = cat->rows[row].size() - 1; + m_currentCursorColumn = last; + return cat->rows[row][last]; + } + default: + break; + } + return current; +} + +int InstanceView::horizontalOffset() const +{ + return horizontalScrollBar()->value(); +} + +int InstanceView::verticalOffset() const +{ + return verticalScrollBar()->value(); +} + +void InstanceView::scrollContentsBy(int dx, int dy) +{ + scrollDirtyRegion(dx, dy); + viewport()->scroll(dx, dy); +} + +void InstanceView::scrollTo(const QModelIndex &index, ScrollHint hint) +{ + if (!index.isValid()) + return; + + const QRect rect = visualRect(index); + if (hint == EnsureVisible && viewport()->rect().contains(rect)) + { + viewport()->update(rect); + return; + } + + verticalScrollBar()->setValue(verticalScrollToValue(index, rect, hint)); +} + +int InstanceView::verticalScrollToValue(const QModelIndex &index, const QRect &rect, QListView::ScrollHint hint) const +{ + const QRect area = viewport()->rect(); + const bool above = (hint == QListView::EnsureVisible && rect.top() < area.top()); + const bool below = (hint == QListView::EnsureVisible && rect.bottom() > area.bottom()); + + int verticalValue = verticalScrollBar()->value(); + QRect adjusted = rect.adjusted(-spacing(), -spacing(), spacing(), spacing()); + if (hint == QListView::PositionAtTop || above) + verticalValue += adjusted.top(); + else if (hint == QListView::PositionAtBottom || below) + verticalValue += qMin(adjusted.top(), adjusted.bottom() - area.height() + 1); + else if (hint == QListView::PositionAtCenter) + verticalValue += adjusted.top() - ((area.height() - adjusted.height()) / 2); + return verticalValue; +} diff --git a/ultimmc/launcher/ui/instanceview/InstanceView.h b/ultimmc/launcher/ui/instanceview/InstanceView.h new file mode 100644 index 0000000..406362e --- /dev/null +++ b/ultimmc/launcher/ui/instanceview/InstanceView.h @@ -0,0 +1,153 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include +#include "VisualGroup.h" +#include + +struct InstanceViewRoles +{ + enum + { + GroupRole = Qt::UserRole, + ProgressValueRole, + ProgressMaximumRole + }; +}; + +class InstanceView : public QAbstractItemView +{ + Q_OBJECT + +public: + InstanceView(QWidget *parent = 0); + ~InstanceView(); + + void setModel(QAbstractItemModel *model) override; + + using visibilityFunction = std::function; + void setSourceOfGroupCollapseStatus(visibilityFunction f) { + fVisibility = f; + } + + /// return geometry rectangle occupied by the specified model item + QRect geometryRect(const QModelIndex &index) const; + /// return visual rectangle occupied by the specified model item + virtual QRect visualRect(const QModelIndex &index) const override; + /// get the model index at the specified visual point + virtual QModelIndex indexAt(const QPoint &point) const override; + QString groupNameAt(const QPoint &point); + void setSelection(const QRect &rect, const QItemSelectionModel::SelectionFlags commands) override; + + virtual int horizontalOffset() const override; + virtual int verticalOffset() const override; + virtual void scrollContentsBy(int dx, int dy) override; + virtual void scrollTo(const QModelIndex &index, ScrollHint hint = EnsureVisible) override; + + virtual QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override; + + virtual QRegion visualRegionForSelection(const QItemSelection &selection) const override; + + int spacing() const + { + return m_spacing; + }; + +public slots: + virtual void updateGeometries() override; + +protected slots: + virtual void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles) override; + virtual void rowsInserted(const QModelIndex &parent, int start, int end) override; + virtual void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) override; + void modelReset(); + void rowsRemoved(); + void currentChanged(const QModelIndex ¤t, const QModelIndex &previous) override; + +signals: + void droppedURLs(QList urls); + void groupStateChanged(QString group, bool collapsed); + +protected: + bool isIndexHidden(const QModelIndex &index) const override; + void mousePressEvent(QMouseEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseDoubleClickEvent(QMouseEvent *event) override; + void paintEvent(QPaintEvent *event) override; + void resizeEvent(QResizeEvent *event) override; + + void dragEnterEvent(QDragEnterEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; + void dropEvent(QDropEvent *event) override; + + void startDrag(Qt::DropActions supportedActions) override; + + void updateScrollbar(); + +private: + friend struct VisualGroup; + QList m_groups; + + visibilityFunction fVisibility; + + // geometry + int m_leftMargin = 5; + int m_rightMargin = 5; + int m_bottomMargin = 5; + int m_categoryMargin = 5; + int m_spacing = 5; + int m_itemWidth = 100; + int m_currentItemsPerRow = -1; + int m_currentCursorColumn= -1; + mutable QCache geometryCache; + + // point where the currently active mouse action started in geometry coordinates + QPoint m_pressedPosition; + QPersistentModelIndex m_pressedIndex; + bool m_pressedAlreadySelected; + VisualGroup *m_pressedCategory; + QItemSelectionModel::SelectionFlag m_ctrlDragSelectionFlag; + QPoint m_lastDragPosition; + + VisualGroup *category(const QModelIndex &index) const; + VisualGroup *category(const QString &cat) const; + VisualGroup *categoryAt(const QPoint &pos, VisualGroup::HitResults & result) const; + + int itemsPerRow() const + { + return m_currentItemsPerRow; + }; + int contentWidth() const; + +private: /* methods */ + int itemWidth() const; + int calculateItemsPerRow() const; + int verticalScrollToValue(const QModelIndex &index, const QRect &rect, QListView::ScrollHint hint) const; + QPixmap renderToPixmap(const QModelIndexList &indices, QRect *r) const; + QList> draggablePaintPairs(const QModelIndexList &indices, QRect *r) const; + + bool isDragEventAccepted(QDropEvent *event); + + QPair rowDropPos(const QPoint &pos); + + QPoint offset() const; +}; diff --git a/ultimmc/launcher/ui/instanceview/VisualGroup.cpp b/ultimmc/launcher/ui/instanceview/VisualGroup.cpp new file mode 100644 index 0000000..8991fb2 --- /dev/null +++ b/ultimmc/launcher/ui/instanceview/VisualGroup.cpp @@ -0,0 +1,317 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "VisualGroup.h" + +#include +#include +#include +#include +#include + +#include "InstanceView.h" + +VisualGroup::VisualGroup(const QString &text, InstanceView *view) : view(view), text(text), collapsed(false) +{ +} + +VisualGroup::VisualGroup(const VisualGroup *other) + : view(other->view), text(other->text), collapsed(other->collapsed) +{ +} + +void VisualGroup::update() +{ + auto temp_items = items(); + auto itemsPerRow = view->itemsPerRow(); + + int numRows = qMax(1, qCeil((qreal)temp_items.size() / (qreal)itemsPerRow)); + rows = QVector(numRows); + + int maxRowHeight = 0; + int positionInRow = 0; + int currentRow = 0; + int offsetFromTop = 0; + for (auto item: temp_items) + { + if(positionInRow == itemsPerRow) + { + rows[currentRow].height = maxRowHeight; + rows[currentRow].top = offsetFromTop; + currentRow ++; + offsetFromTop += maxRowHeight + 5; + positionInRow = 0; + maxRowHeight = 0; + } + auto itemHeight = view->itemDelegate()->sizeHint(view->viewOptions(), item).height(); + if(itemHeight > maxRowHeight) + { + maxRowHeight = itemHeight; + } + rows[currentRow].items.append(item); + positionInRow++; + } + rows[currentRow].height = maxRowHeight; + rows[currentRow].top = offsetFromTop; +} + +QPair VisualGroup::positionOf(const QModelIndex &index) const +{ + int y = 0; + for (auto & row: rows) + { + for(auto x = 0; x < row.items.size(); x++) + { + if(row.items[x] == index) + { + return qMakePair(x,y); + } + } + y++; + } + qWarning() << "Item" << index.row() << index.data(Qt::DisplayRole).toString() << "not found in visual group" << text; + return qMakePair(0, 0); +} + +int VisualGroup::rowTopOf(const QModelIndex &index) const +{ + auto position = positionOf(index); + return rows[position.second].top; +} + +int VisualGroup::rowHeightOf(const QModelIndex &index) const +{ + auto position = positionOf(index); + return rows[position.second].height; +} + +VisualGroup::HitResults VisualGroup::hitScan(const QPoint &pos) const +{ + VisualGroup::HitResults results = VisualGroup::NoHit; + int y_start = verticalPosition(); + int body_start = y_start + headerHeight(); + int body_end = body_start + contentHeight() + 5; // FIXME: wtf is this 5? + int y = pos.y(); + // int x = pos.x(); + if (y < y_start) + { + results = VisualGroup::NoHit; + } + else if (y < body_start) + { + results = VisualGroup::HeaderHit; + int collapseSize = headerHeight() - 4; + + // the icon + QRect iconRect = QRect(view->m_leftMargin + 2, 2 + y_start, collapseSize, collapseSize); + if (iconRect.contains(pos)) + { + results |= VisualGroup::CheckboxHit; + } + } + else if (y < body_end) + { + results |= VisualGroup::BodyHit; + } + return results; +} + +void VisualGroup::drawHeader(QPainter *painter, const QStyleOptionViewItem &option) +{ + painter->setRenderHint(QPainter::Antialiasing); + + const QRect optRect = option.rect; + QFont font(QApplication::font()); + font.setBold(true); + const QFontMetrics fontMetrics = QFontMetrics(font); + + QColor outlineColor = option.palette.text().color(); + outlineColor.setAlphaF(0.35); + + //BEGIN: top left corner + { + painter->save(); + painter->setPen(outlineColor); + const QPointF topLeft(optRect.topLeft()); + QRectF arc(topLeft, QSizeF(4, 4)); + arc.translate(0.5, 0.5); + painter->drawArc(arc, 1440, 1440); + painter->restore(); + } + //END: top left corner + + //BEGIN: left vertical line + { + QPoint start(optRect.topLeft()); + start.ry() += 3; + QPoint verticalGradBottom(optRect.topLeft()); + verticalGradBottom.ry() += fontMetrics.height() + 5; + QLinearGradient gradient(start, verticalGradBottom); + gradient.setColorAt(0, outlineColor); + gradient.setColorAt(1, Qt::transparent); + painter->fillRect(QRect(start, QSize(1, fontMetrics.height() + 5)), gradient); + } + //END: left vertical line + + //BEGIN: horizontal line + { + QPoint start(optRect.topLeft()); + start.rx() += 3; + QPoint horizontalGradTop(optRect.topLeft()); + horizontalGradTop.rx() += optRect.width() - 6; + painter->fillRect(QRect(start, QSize(optRect.width() - 6, 1)), outlineColor); + } + //END: horizontal line + + //BEGIN: top right corner + { + painter->save(); + painter->setPen(outlineColor); + QPointF topRight(optRect.topRight()); + topRight.rx() -= 4; + QRectF arc(topRight, QSizeF(4, 4)); + arc.translate(0.5, 0.5); + painter->drawArc(arc, 0, 1440); + painter->restore(); + } + //END: top right corner + + //BEGIN: right vertical line + { + QPoint start(optRect.topRight()); + start.ry() += 3; + QPoint verticalGradBottom(optRect.topRight()); + verticalGradBottom.ry() += fontMetrics.height() + 5; + QLinearGradient gradient(start, verticalGradBottom); + gradient.setColorAt(0, outlineColor); + gradient.setColorAt(1, Qt::transparent); + painter->fillRect(QRect(start, QSize(1, fontMetrics.height() + 5)), gradient); + } + //END: right vertical line + + //BEGIN: checkboxy thing + { + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, false); + painter->setFont(font); + QColor penColor(option.palette.text().color()); + penColor.setAlphaF(0.6); + painter->setPen(penColor); + QRect iconSubRect(option.rect); + iconSubRect.setTop(iconSubRect.top() + 7); + iconSubRect.setLeft(iconSubRect.left() + 7); + + int sizing = fontMetrics.height(); + int even = ( (sizing - 1) % 2 ); + + iconSubRect.setHeight(sizing - even); + iconSubRect.setWidth(sizing - even); + painter->drawRect(iconSubRect); + + + /* + if(collapsed) + painter->drawText(iconSubRect, Qt::AlignHCenter | Qt::AlignVCenter, "+"); + else + painter->drawText(iconSubRect, Qt::AlignHCenter | Qt::AlignVCenter, "-"); + */ + painter->setBrush(option.palette.text()); + painter->fillRect(iconSubRect.x(), iconSubRect.y() + iconSubRect.height() / 2, + iconSubRect.width(), 2, penColor); + if (collapsed) + { + painter->fillRect(iconSubRect.x() + iconSubRect.width() / 2, iconSubRect.y(), 2, + iconSubRect.height(), penColor); + } + + painter->restore(); + } + //END: checkboxy thing + + //BEGIN: text + { + QRect textRect(option.rect); + textRect.setTop(textRect.top() + 7); + textRect.setLeft(textRect.left() + 7 + fontMetrics.height() + 7); + textRect.setHeight(fontMetrics.height()); + textRect.setRight(textRect.right() - 7); + + painter->save(); + painter->setFont(font); + QColor penColor(option.palette.text().color()); + penColor.setAlphaF(0.6); + painter->setPen(penColor); + painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text); + painter->restore(); + } + //END: text +} + +int VisualGroup::totalHeight() const +{ + return headerHeight() + 5 + contentHeight(); // FIXME: wtf is that '5'? +} + +int VisualGroup::headerHeight() const +{ + QFont font(QApplication::font()); + font.setBold(true); + QFontMetrics fontMetrics(font); + + const int height = fontMetrics.height() + 1 /* 1 pixel-width gradient */ + + 11 /* top and bottom separation */; + return height; + /* + int raw = view->viewport()->fontMetrics().height() + 4; + // add english. maybe. depends on font height. + if (raw % 2 == 0) + raw++; + return std::min(raw, 25); + */ +} + +int VisualGroup::contentHeight() const +{ + if (collapsed) + { + return 0; + } + auto last = rows[numRows() - 1]; + return last.top + last.height; +} + +int VisualGroup::numRows() const +{ + return rows.size(); +} + +int VisualGroup::verticalPosition() const +{ + return m_verticalPosition; +} + +QList VisualGroup::items() const +{ + QList indices; + for (int i = 0; i < view->model()->rowCount(); ++i) + { + const QModelIndex index = view->model()->index(i, 0); + if (index.data(InstanceViewRoles::GroupRole).toString() == text) + { + indices.append(index); + } + } + return indices; +} diff --git a/ultimmc/launcher/ui/instanceview/VisualGroup.h b/ultimmc/launcher/ui/instanceview/VisualGroup.h new file mode 100644 index 0000000..5a743aa --- /dev/null +++ b/ultimmc/launcher/ui/instanceview/VisualGroup.h @@ -0,0 +1,106 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +class InstanceView; +class QPainter; +class QModelIndex; + +struct VisualRow +{ + QList items; + int height = 0; + int top = 0; + inline int size() const + { + return items.size(); + } + inline QModelIndex &operator[](int i) + { + return items[i]; + } +}; + +struct VisualGroup +{ +/* constructors */ + VisualGroup(const QString &text, InstanceView *view); + VisualGroup(const VisualGroup *other); + +/* data */ + InstanceView *view = nullptr; + QString text; + bool collapsed = false; + QVector rows; + int firstItemIndex = 0; + int m_verticalPosition = 0; + +/* logic */ + /// update the internal list of items and flow them into the rows. + void update(); + + /// draw the header at y-position. + void drawHeader(QPainter *painter, const QStyleOptionViewItem &option); + + /// height of the group, in total. includes a small bit of padding. + int totalHeight() const; + + /// height of the group header, in pixels + int headerHeight() const; + + /// height of the group content, in pixels + int contentHeight() const; + + /// the number of visual rows this group has + int numRows() const; + + /// actually calculate the above value + int calculateNumRows() const; + + /// the height at which this group starts, in pixels + int verticalPosition() const; + + /// relative geometry - top of the row of the given item + int rowTopOf(const QModelIndex &index) const; + + /// height of the row of the given item + int rowHeightOf(const QModelIndex &index) const; + + /// x/y position of the given item inside the group (in items!) + QPair positionOf(const QModelIndex &index) const; + + enum HitResult + { + NoHit = 0x0, + TextHit = 0x1, + CheckboxHit = 0x2, + HeaderHit = 0x4, + BodyHit = 0x8 + }; + Q_DECLARE_FLAGS(HitResults, HitResult) + + /// shoot! BANG! what did we hit? + HitResults hitScan (const QPoint &pos) const; + + QList items() const; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(VisualGroup::HitResults) diff --git a/ultimmc/launcher/ui/pagedialog/PageDialog.cpp b/ultimmc/launcher/ui/pagedialog/PageDialog.cpp new file mode 100644 index 0000000..18d61dc --- /dev/null +++ b/ultimmc/launcher/ui/pagedialog/PageDialog.cpp @@ -0,0 +1,62 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PageDialog.h" + +#include +#include +#include +#include + +#include "Application.h" +#include "settings/SettingsObject.h" + +#include "ui/widgets/IconLabel.h" +#include "ui/widgets/PageContainer.h" + +PageDialog::PageDialog(BasePageProvider *pageProvider, QString defaultId, QWidget *parent) + : QDialog(parent) +{ + setWindowTitle(pageProvider->dialogTitle()); + m_container = new PageContainer(pageProvider, defaultId, this); + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->addWidget(m_container); + mainLayout->setSpacing(0); + mainLayout->setContentsMargins(0, 0, 0, 0); + setLayout(mainLayout); + + QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Help | QDialogButtonBox::Close); + buttons->button(QDialogButtonBox::Close)->setDefault(true); + buttons->setContentsMargins(6, 0, 6, 0); + m_container->addButtons(buttons); + + connect(buttons->button(QDialogButtonBox::Close), SIGNAL(clicked()), this, SLOT(close())); + connect(buttons->button(QDialogButtonBox::Help), SIGNAL(clicked()), m_container, SLOT(help())); + + restoreGeometry(QByteArray::fromBase64(APPLICATION->settings()->get("PagedGeometry").toByteArray())); +} + +void PageDialog::closeEvent(QCloseEvent *event) +{ + qDebug() << "Paged dialog close requested"; + if (m_container->prepareToClose()) + { + qDebug() << "Paged dialog close approved"; + APPLICATION->settings()->set("PagedGeometry", saveGeometry().toBase64()); + qDebug() << "Paged dialog geometry saved"; + QDialog::closeEvent(event); + } +} diff --git a/ultimmc/launcher/ui/pagedialog/PageDialog.h b/ultimmc/launcher/ui/pagedialog/PageDialog.h new file mode 100644 index 0000000..00d8b72 --- /dev/null +++ b/ultimmc/launcher/ui/pagedialog/PageDialog.h @@ -0,0 +1,35 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "ui/pages/BasePageProvider.h" + +class PageContainer; +class PageDialog : public QDialog +{ + Q_OBJECT +public: + explicit PageDialog(BasePageProvider *pageProvider, QString defaultId = QString(), QWidget *parent = 0); + virtual ~PageDialog() {} + +private +slots: + virtual void closeEvent(QCloseEvent *event); + +private: + PageContainer * m_container; +}; diff --git a/ultimmc/launcher/ui/pages/BasePage.h b/ultimmc/launcher/ui/pages/BasePage.h new file mode 100644 index 0000000..408965d --- /dev/null +++ b/ultimmc/launcher/ui/pages/BasePage.h @@ -0,0 +1,58 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "BasePageContainer.h" + +class BasePage +{ +public: + virtual ~BasePage() {} + virtual QString id() const = 0; + virtual QString displayName() const = 0; + virtual QIcon icon() const = 0; + virtual bool apply() { return true; } + virtual bool shouldDisplay() const { return true; } + virtual QString helpPage() const { return QString(); } + void opened() + { + isOpened = true; + openedImpl(); + } + void closed() + { + isOpened = false; + closedImpl(); + } + virtual void openedImpl() {} + virtual void closedImpl() {} + virtual void setParentContainer(BasePageContainer * container) + { + m_container = container; + }; +public: + int stackIndex = -1; + int listIndex = -1; +protected: + BasePageContainer * m_container = nullptr; + bool isOpened = false; +}; + +typedef std::shared_ptr BasePagePtr; diff --git a/ultimmc/launcher/ui/pages/BasePageContainer.h b/ultimmc/launcher/ui/pages/BasePageContainer.h new file mode 100644 index 0000000..f8c7ade --- /dev/null +++ b/ultimmc/launcher/ui/pages/BasePageContainer.h @@ -0,0 +1,10 @@ +#pragma once + +class BasePageContainer +{ +public: + virtual ~BasePageContainer(){}; + virtual bool selectPage(QString pageId) = 0; + virtual void refreshContainer() = 0; + virtual bool requestClose() = 0; +}; diff --git a/ultimmc/launcher/ui/pages/BasePageProvider.h b/ultimmc/launcher/ui/pages/BasePageProvider.h new file mode 100644 index 0000000..873e8dc --- /dev/null +++ b/ultimmc/launcher/ui/pages/BasePageProvider.h @@ -0,0 +1,68 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "ui/pages/BasePage.h" +#include +#include + +class BasePageProvider +{ +public: + virtual QList getPages() = 0; + virtual QString dialogTitle() = 0; +}; + +class GenericPageProvider : public BasePageProvider +{ + typedef std::function PageCreator; +public: + explicit GenericPageProvider(const QString &dialogTitle) + : m_dialogTitle(dialogTitle) + { + } + virtual ~GenericPageProvider() {} + + QList getPages() override + { + QList pages; + for (PageCreator creator : m_creators) + { + pages.append(creator()); + } + return pages; + } + QString dialogTitle() override { return m_dialogTitle; } + + void setDialogTitle(const QString &title) + { + m_dialogTitle = title; + } + void addPageCreator(PageCreator page) + { + m_creators.append(page); + } + + template + void addPage() + { + addPageCreator([](){return new PageClass();}); + } + +private: + QList m_creators; + QString m_dialogTitle; +}; diff --git a/ultimmc/launcher/ui/pages/global/AccountListPage.cpp b/ultimmc/launcher/ui/pages/global/AccountListPage.cpp new file mode 100644 index 0000000..46c2cf1 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/AccountListPage.cpp @@ -0,0 +1,268 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AccountListPage.h" +#include "ui_AccountListPage.h" + +#include +#include + +#include + +#include "net/NetJob.h" + +#include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/LoginDialog.h" +#include "ui/dialogs/MSALoginDialog.h" +#include "ui/dialogs/LocalLoginDialog.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/SkinUploadDialog.h" + +#include "tasks/Task.h" +#include "minecraft/auth/AccountTask.h" +#include "minecraft/services/SkinDelete.h" + +#include "Application.h" + +#include "BuildConfig.h" + +#include "Secrets.h" + +AccountListPage::AccountListPage(QWidget *parent) + : QMainWindow(parent), ui(new Ui::AccountListPage) +{ + ui->setupUi(this); + ui->listView->setEmptyString(tr( + "Welcome!\n" + "If you're new here, you can click the \"Add Local\" button to add your local account.\n" + "Or click the \"Add Ely.by\" button to add your Ely.by account." + )); + ui->listView->setEmptyMode(VersionListView::String); + ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); + + m_accounts = APPLICATION->accounts(); + + ui->listView->setModel(m_accounts.get()); + ui->listView->header()->setSectionResizeMode(0, QHeaderView::Stretch); + ui->listView->header()->setSectionResizeMode(1, QHeaderView::Stretch); + ui->listView->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + ui->listView->setSelectionMode(QAbstractItemView::SingleSelection); + + // Expand the account column + + QItemSelectionModel *selectionModel = ui->listView->selectionModel(); + + connect(selectionModel, &QItemSelectionModel::selectionChanged, [this](const QItemSelection &sel, const QItemSelection &dsel) { + updateButtonStates(); + }); + connect(ui->listView, &VersionListView::customContextMenuRequested, this, &AccountListPage::ShowContextMenu); + + connect(m_accounts.get(), &AccountList::listChanged, this, &AccountListPage::listChanged); + connect(m_accounts.get(), &AccountList::listActivityChanged, this, &AccountListPage::listChanged); + connect(m_accounts.get(), &AccountList::defaultAccountChanged, this, &AccountListPage::listChanged); + + updateButtonStates(); + + // Xbox authentication won't work without a client identifier, so disable the button if it is missing + ui->actionAddMicrosoft->setVisible(Secrets::hasMSAClientID()); +} + +AccountListPage::~AccountListPage() +{ + delete ui; +} + +void AccountListPage::ShowContextMenu(const QPoint& pos) +{ + auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); + menu->exec(ui->listView->mapToGlobal(pos)); + delete menu; +} + +void AccountListPage::changeEvent(QEvent* event) +{ + if (event->type() == QEvent::LanguageChange) + { + ui->retranslateUi(this); + } + QMainWindow::changeEvent(event); +} + +QMenu * AccountListPage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction(ui->toolBar->toggleViewAction() ); + return filteredMenu; +} + + +void AccountListPage::listChanged() +{ + updateButtonStates(); +} + +void AccountListPage::on_actionAddLocal_triggered() +{ + MinecraftAccountPtr account = LocalLoginDialog::newAccount( + this, + tr("Please enter your desired username to add your account.") + ); + + if (account) + { + m_accounts->addAccount(account); + if (m_accounts->count() == 1) { + m_accounts->setDefaultAccount(account); + } + } +} + +void AccountListPage::on_actionAddMojang_triggered() +{ + MinecraftAccountPtr account = LoginDialog::newAccount( + this, + tr("Please enter your account email and password to add your account.") + ); + + if (account) + { + m_accounts->addAccount(account); + if (m_accounts->count() == 1) { + m_accounts->setDefaultAccount(account); + } + } +} + +void AccountListPage::on_actionAddMicrosoft_triggered() +{ + if(BuildConfig.BUILD_PLATFORM == "osx64") { + CustomMessageBox::selectable( + this, + tr("Microsoft Accounts not available"), + tr( + "Microsoft accounts are only usable on macOS 10.13 or newer, with fully updated MultiMC.\n\n" + "Please update both your operating system and MultiMC." + ), + QMessageBox::Warning + )->exec(); + return; + } + MinecraftAccountPtr account = MSALoginDialog::newAccount( + this, + tr("Please enter your Mojang account email and password to add your account.") + ); + + if (account) + { + m_accounts->addAccount(account); + if (m_accounts->count() == 1) { + m_accounts->setDefaultAccount(account); + } + } +} + +void AccountListPage::on_actionRemove_triggered() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) + { + QModelIndex selected = selection.first(); + m_accounts->removeAccount(selected); + } +} + +void AccountListPage::on_actionRefresh_triggered() { + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) { + QModelIndex selected = selection.first(); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); + m_accounts->requestRefresh(account->internalId()); + } +} + + +void AccountListPage::on_actionSetDefault_triggered() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) + { + QModelIndex selected = selection.first(); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); + m_accounts->setDefaultAccount(account); + } +} + +void AccountListPage::on_actionNoDefault_triggered() +{ + m_accounts->setDefaultAccount(nullptr); +} + +void AccountListPage::updateButtonStates() +{ + // If there is no selection, disable buttons that require something selected. + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + bool hasSelection = selection.size() > 0; + bool accountIsReady = false; + bool accountIsOnline = false; + if (hasSelection) + { + QModelIndex selected = selection.first(); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); + accountIsReady = !account->isActive(); + accountIsOnline = account->typeString() != "local" && account->typeString() != "elyby"; + } + ui->actionRemove->setEnabled(accountIsReady); + ui->actionSetDefault->setEnabled(accountIsReady); + ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline); + ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline); + ui->actionRefresh->setEnabled(accountIsReady); + + if(m_accounts->defaultAccount().get() == nullptr) { + ui->actionNoDefault->setEnabled(false); + ui->actionNoDefault->setChecked(true); + } + else { + ui->actionNoDefault->setEnabled(true); + ui->actionNoDefault->setChecked(false); + } +} + +void AccountListPage::on_actionUploadSkin_triggered() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) + { + QModelIndex selected = selection.first(); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); + SkinUploadDialog dialog(account, this); + dialog.exec(); + } +} + +void AccountListPage::on_actionDeleteSkin_triggered() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() <= 0) + return; + + QModelIndex selected = selection.first(); + MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value(); + ProgressDialog prog(this); + auto deleteSkinTask = std::make_shared(this, account->accessToken()); + if (prog.execWithTask((Task*)deleteSkinTask.get()) != QDialog::Accepted) { + CustomMessageBox::selectable(this, tr("Skin Delete"), tr("Failed to delete current skin!"), QMessageBox::Warning)->exec(); + return; + } +} diff --git a/ultimmc/launcher/ui/pages/global/AccountListPage.h b/ultimmc/launcher/ui/pages/global/AccountListPage.h new file mode 100644 index 0000000..84b1749 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/AccountListPage.h @@ -0,0 +1,86 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "ui/pages/BasePage.h" + +#include "minecraft/auth/AccountList.h" +#include "Application.h" + +namespace Ui +{ +class AccountListPage; +} + +class AuthenticateTask; + +class AccountListPage : public QMainWindow, public BasePage +{ + Q_OBJECT +public: + explicit AccountListPage(QWidget *parent = 0); + ~AccountListPage(); + + QString displayName() const override + { + return tr("Accounts"); + } + QIcon icon() const override + { + auto icon = APPLICATION->getThemedIcon("accounts"); + if(icon.isNull()) + { + icon = APPLICATION->getThemedIcon("noaccount"); + } + return icon; + } + QString id() const override + { + return "accounts"; + } + QString helpPage() const override + { + return "Getting-Started#adding-an-account"; + } + +public slots: + void on_actionAddLocal_triggered(); + void on_actionAddMojang_triggered(); + void on_actionAddMicrosoft_triggered(); + void on_actionRemove_triggered(); + void on_actionRefresh_triggered(); + void on_actionSetDefault_triggered(); + void on_actionNoDefault_triggered(); + void on_actionUploadSkin_triggered(); + void on_actionDeleteSkin_triggered(); + + void listChanged(); + + //! Updates the states of the dialog's buttons. + void updateButtonStates(); + +protected slots: + void ShowContextMenu(const QPoint &pos); + +private: + void changeEvent(QEvent * event) override; + QMenu * createPopupMenu() override; + shared_qobject_ptr m_accounts; + Ui::AccountListPage *ui; +}; diff --git a/ultimmc/launcher/ui/pages/global/AccountListPage.ui b/ultimmc/launcher/ui/pages/global/AccountListPage.ui new file mode 100644 index 0000000..3fefeff --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/AccountListPage.ui @@ -0,0 +1,135 @@ + + + AccountListPage + + + + 0 + 0 + 800 + 600 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + false + + + false + + + true + + + false + + + + + + + + RightToolBarArea + + + false + + + + + + + + + + + + + + + Add Local + + + + + Add Ely.by + + + + + Remove + + + + + Set Default + + + + + true + + + No Default + + + + + Upload Skin + + + + + Delete Skin + + + Delete the currently active skin and go back to the default one + + + + + Add Microsoft + + + + + Refresh + + + Refresh the account tokens + + + + + + VersionListView + QTreeView +
ui/widgets/VersionListView.h
+
+ + WideBar + QToolBar +
ui/widgets/WideBar.h
+
+
+ + +
diff --git a/ultimmc/launcher/ui/pages/global/CustomCommandsPage.cpp b/ultimmc/launcher/ui/pages/global/CustomCommandsPage.cpp new file mode 100644 index 0000000..8541e3c --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/CustomCommandsPage.cpp @@ -0,0 +1,51 @@ +#include "CustomCommandsPage.h" +#include +#include +#include + +CustomCommandsPage::CustomCommandsPage(QWidget* parent): QWidget(parent) +{ + + auto verticalLayout = new QVBoxLayout(this); + verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + verticalLayout->setContentsMargins(0, 0, 0, 0); + + auto tabWidget = new QTabWidget(this); + tabWidget->setObjectName(QStringLiteral("tabWidget")); + commands = new CustomCommands(this); + commands->setContentsMargins(6, 6, 6, 6); + tabWidget->addTab(commands, "Foo"); + tabWidget->tabBar()->hide(); + verticalLayout->addWidget(tabWidget); + loadSettings(); +} + +CustomCommandsPage::~CustomCommandsPage() +{ +} + +bool CustomCommandsPage::apply() +{ + applySettings(); + return true; +} + +void CustomCommandsPage::applySettings() +{ + auto s = APPLICATION->settings(); + s->set("PreLaunchCommand", commands->prelaunchCommand()); + s->set("WrapperCommand", commands->wrapperCommand()); + s->set("PostExitCommand", commands->postexitCommand()); +} + +void CustomCommandsPage::loadSettings() +{ + auto s = APPLICATION->settings(); + commands->initialize( + false, + true, + s->get("PreLaunchCommand").toString(), + s->get("WrapperCommand").toString(), + s->get("PostExitCommand").toString() + ); +} diff --git a/ultimmc/launcher/ui/pages/global/CustomCommandsPage.h b/ultimmc/launcher/ui/pages/global/CustomCommandsPage.h new file mode 100644 index 0000000..a1155e0 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/CustomCommandsPage.h @@ -0,0 +1,55 @@ +/* Copyright 2018-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "ui/pages/BasePage.h" +#include +#include "ui/widgets/CustomCommands.h" + +class CustomCommandsPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit CustomCommandsPage(QWidget *parent = 0); + ~CustomCommandsPage(); + + QString displayName() const override + { + return tr("Custom Commands"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("custom-commands"); + } + QString id() const override + { + return "custom-commands"; + } + QString helpPage() const override + { + return "Custom-commands"; + } + bool apply() override; + +private: + void applySettings(); + void loadSettings(); + CustomCommands * commands; +}; diff --git a/ultimmc/launcher/ui/pages/global/ExternalToolsPage.cpp b/ultimmc/launcher/ui/pages/global/ExternalToolsPage.cpp new file mode 100644 index 0000000..41d900a --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/ExternalToolsPage.cpp @@ -0,0 +1,233 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ExternalToolsPage.h" +#include "ui_ExternalToolsPage.h" + +#include +#include +#include +#include + +#include "settings/SettingsObject.h" +#include "tools/BaseProfiler.h" +#include +#include "Application.h" +#include + +ExternalToolsPage::ExternalToolsPage(QWidget *parent) : + QWidget(parent), + ui(new Ui::ExternalToolsPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + + #if QT_VERSION >= QT_VERSION_CHECK(5, 2, 0) + ui->jsonEditorTextBox->setClearButtonEnabled(true); + #endif + + ui->mceditLink->setOpenExternalLinks(true); + ui->jvisualvmLink->setOpenExternalLinks(true); + ui->jprofilerLink->setOpenExternalLinks(true); + loadSettings(); +} + +ExternalToolsPage::~ExternalToolsPage() +{ + delete ui; +} + +void ExternalToolsPage::loadSettings() +{ + auto s = APPLICATION->settings(); + ui->jprofilerPathEdit->setText(s->get("JProfilerPath").toString()); + ui->jvisualvmPathEdit->setText(s->get("JVisualVMPath").toString()); + ui->mceditPathEdit->setText(s->get("MCEditPath").toString()); + + // Editors + ui->jsonEditorTextBox->setText(s->get("JsonEditor").toString()); +} +void ExternalToolsPage::applySettings() +{ + auto s = APPLICATION->settings(); + + s->set("JProfilerPath", ui->jprofilerPathEdit->text()); + s->set("JVisualVMPath", ui->jvisualvmPathEdit->text()); + s->set("MCEditPath", ui->mceditPathEdit->text()); + + // Editors + QString jsonEditor = ui->jsonEditorTextBox->text(); + if (!jsonEditor.isEmpty() && + (!QFileInfo(jsonEditor).exists() || !QFileInfo(jsonEditor).isExecutable())) + { + QString found = QStandardPaths::findExecutable(jsonEditor); + if (!found.isEmpty()) + { + jsonEditor = found; + } + } + s->set("JsonEditor", jsonEditor); +} + +void ExternalToolsPage::on_jprofilerPathBtn_clicked() +{ + QString raw_dir = ui->jprofilerPathEdit->text(); + QString error; + do + { + raw_dir = QFileDialog::getExistingDirectory(this, tr("JProfiler Folder"), raw_dir); + if (raw_dir.isEmpty()) + { + break; + } + QString cooked_dir = FS::NormalizePath(raw_dir); + if (!APPLICATION->profilers()["jprofiler"]->check(cooked_dir, &error)) + { + QMessageBox::critical(this, tr("Error"), tr("Error while checking JProfiler install:\n%1").arg(error)); + continue; + } + else + { + ui->jprofilerPathEdit->setText(cooked_dir); + break; + } + } while (1); +} +void ExternalToolsPage::on_jprofilerCheckBtn_clicked() +{ + QString error; + if (!APPLICATION->profilers()["jprofiler"]->check(ui->jprofilerPathEdit->text(), &error)) + { + QMessageBox::critical(this, tr("Error"), tr("Error while checking JProfiler install:\n%1").arg(error)); + } + else + { + QMessageBox::information(this, tr("OK"), tr("JProfiler setup seems to be OK")); + } +} + +void ExternalToolsPage::on_jvisualvmPathBtn_clicked() +{ + QString raw_dir = ui->jvisualvmPathEdit->text(); + QString error; + do + { + raw_dir = QFileDialog::getOpenFileName(this, tr("JVisualVM Executable"), raw_dir); + if (raw_dir.isEmpty()) + { + break; + } + QString cooked_dir = FS::NormalizePath(raw_dir); + if (!APPLICATION->profilers()["jvisualvm"]->check(cooked_dir, &error)) + { + QMessageBox::critical(this, tr("Error"), tr("Error while checking JVisualVM install:\n%1").arg(error)); + continue; + } + else + { + ui->jvisualvmPathEdit->setText(cooked_dir); + break; + } + } while (1); +} +void ExternalToolsPage::on_jvisualvmCheckBtn_clicked() +{ + QString error; + if (!APPLICATION->profilers()["jvisualvm"]->check(ui->jvisualvmPathEdit->text(), &error)) + { + QMessageBox::critical(this, tr("Error"), tr("Error while checking JVisualVM install:\n%1").arg(error)); + } + else + { + QMessageBox::information(this, tr("OK"), tr("JVisualVM setup seems to be OK")); + } +} + +void ExternalToolsPage::on_mceditPathBtn_clicked() +{ + QString raw_dir = ui->mceditPathEdit->text(); + QString error; + do + { +#ifdef Q_OS_OSX + raw_dir = QFileDialog::getOpenFileName(this, tr("MCEdit Application"), raw_dir); +#else + raw_dir = QFileDialog::getExistingDirectory(this, tr("MCEdit Folder"), raw_dir); +#endif + if (raw_dir.isEmpty()) + { + break; + } + QString cooked_dir = FS::NormalizePath(raw_dir); + if (!APPLICATION->mcedit()->check(cooked_dir, error)) + { + QMessageBox::critical(this, tr("Error"), tr("Error while checking MCEdit install:\n%1").arg(error)); + continue; + } + else + { + ui->mceditPathEdit->setText(cooked_dir); + break; + } + } while (1); +} +void ExternalToolsPage::on_mceditCheckBtn_clicked() +{ + QString error; + if (!APPLICATION->mcedit()->check(ui->mceditPathEdit->text(), error)) + { + QMessageBox::critical(this, tr("Error"), tr("Error while checking MCEdit install:\n%1").arg(error)); + } + else + { + QMessageBox::information(this, tr("OK"), tr("MCEdit setup seems to be OK")); + } +} + +void ExternalToolsPage::on_jsonEditorBrowseBtn_clicked() +{ + QString raw_file = QFileDialog::getOpenFileName( + this, tr("JSON Editor"), + ui->jsonEditorTextBox->text().isEmpty() +#if defined(Q_OS_LINUX) + ? QString("/usr/bin") +#else + ? QStandardPaths::standardLocations(QStandardPaths::ApplicationsLocation).first() +#endif + : ui->jsonEditorTextBox->text()); + + if (raw_file.isEmpty()) + { + return; + } + QString cooked_file = FS::NormalizePath(raw_file); + + // it has to exist and be an executable + if (QFileInfo(cooked_file).exists() && QFileInfo(cooked_file).isExecutable()) + { + ui->jsonEditorTextBox->setText(cooked_file); + } + else + { + QMessageBox::warning(this, tr("Invalid"), + tr("The file chosen does not seem to be an executable")); + } +} + +bool ExternalToolsPage::apply() +{ + applySettings(); + return true; +} diff --git a/ultimmc/launcher/ui/pages/global/ExternalToolsPage.h b/ultimmc/launcher/ui/pages/global/ExternalToolsPage.h new file mode 100644 index 0000000..5ae6148 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/ExternalToolsPage.h @@ -0,0 +1,74 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "ui/pages/BasePage.h" +#include + +namespace Ui { +class ExternalToolsPage; +} + +class ExternalToolsPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit ExternalToolsPage(QWidget *parent = 0); + ~ExternalToolsPage(); + + QString displayName() const override + { + return tr("External Tools"); + } + QIcon icon() const override + { + auto icon = APPLICATION->getThemedIcon("externaltools"); + if(icon.isNull()) + { + icon = APPLICATION->getThemedIcon("loadermods"); + } + return icon; + } + QString id() const override + { + return "external-tools"; + } + QString helpPage() const override + { + return "Tools"; + } + virtual bool apply() override; + +private: + void loadSettings(); + void applySettings(); + +private: + Ui::ExternalToolsPage *ui; + +private +slots: + void on_jprofilerPathBtn_clicked(); + void on_jprofilerCheckBtn_clicked(); + void on_jvisualvmPathBtn_clicked(); + void on_jvisualvmCheckBtn_clicked(); + void on_mceditPathBtn_clicked(); + void on_mceditCheckBtn_clicked(); + void on_jsonEditorBrowseBtn_clicked(); +}; diff --git a/ultimmc/launcher/ui/pages/global/ExternalToolsPage.ui b/ultimmc/launcher/ui/pages/global/ExternalToolsPage.ui new file mode 100644 index 0000000..e79e938 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/ExternalToolsPage.ui @@ -0,0 +1,194 @@ + + + ExternalToolsPage + + + + 0 + 0 + 673 + 751 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + Tab 1 + + + + + + JProfiler + + + + + + + + + + + ... + + + + + + + + + Check + + + + + + + <html><head/><body><p><a href="https://www.ej-technologies.com/products/jprofiler/overview.html">https://www.ej-technologies.com/products/jprofiler/overview.html</a></p></body></html> + + + + + + + + + + JVisualVM + + + + + + + + + + + ... + + + + + + + + + Check + + + + + + + <html><head/><body><p><a href="https://visualvm.github.io/">https://visualvm.github.io/</a></p></body></html> + + + + + + + + + + MCEdit + + + + + + + + + + + ... + + + + + + + + + Check + + + + + + + <html><head/><body><p><a href="https://www.mcedit.net/">https://www.mcedit.net/</a></p></body></html> + + + + + + + + + + External Editors (leave empty for system default) + + + + + + + + + Text Editor: + + + + + + + ... + + + + + + + + + + Qt::Vertical + + + + 20 + 216 + + + + + + + + + + + + + diff --git a/ultimmc/launcher/ui/pages/global/JavaPage.cpp b/ultimmc/launcher/ui/pages/global/JavaPage.cpp new file mode 100644 index 0000000..bd79f11 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/JavaPage.cpp @@ -0,0 +1,153 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "JavaPage.h" +#include "JavaCommon.h" +#include "ui_JavaPage.h" + +#include +#include +#include +#include + +#include "ui/dialogs/VersionSelectDialog.h" + +#include "java/JavaUtils.h" +#include "java/JavaInstallList.h" + +#include "settings/SettingsObject.h" +#include +#include "Application.h" +#include + +JavaPage::JavaPage(QWidget *parent) : QWidget(parent), ui(new Ui::JavaPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + + auto sysMiB = Sys::getSystemRam() / Sys::mebibyte; + ui->maxMemSpinBox->setMaximum(sysMiB); + loadSettings(); +} + +JavaPage::~JavaPage() +{ + delete ui; +} + +bool JavaPage::apply() +{ + applySettings(); + return true; +} + +void JavaPage::applySettings() +{ + auto s = APPLICATION->settings(); + + // Memory + int min = ui->minMemSpinBox->value(); + int max = ui->maxMemSpinBox->value(); + if(min < max) + { + s->set("MinMemAlloc", min); + s->set("MaxMemAlloc", max); + } + else + { + s->set("MinMemAlloc", max); + s->set("MaxMemAlloc", min); + } + s->set("PermGen", ui->permGenSpinBox->value()); + + // Java Settings + s->set("JavaPath", ui->javaPathTextBox->text()); + s->set("JvmArgs", ui->jvmArgsTextBox->text()); + JavaCommon::checkJVMArgs(s->get("JvmArgs").toString(), this->parentWidget()); +} +void JavaPage::loadSettings() +{ + auto s = APPLICATION->settings(); + // Memory + int min = s->get("MinMemAlloc").toInt(); + int max = s->get("MaxMemAlloc").toInt(); + if(min < max) + { + ui->minMemSpinBox->setValue(min); + ui->maxMemSpinBox->setValue(max); + } + else + { + ui->minMemSpinBox->setValue(max); + ui->maxMemSpinBox->setValue(min); + } + ui->permGenSpinBox->setValue(s->get("PermGen").toInt()); + + // Java Settings + ui->javaPathTextBox->setText(s->get("JavaPath").toString()); + ui->jvmArgsTextBox->setText(s->get("JvmArgs").toString()); +} + +void JavaPage::on_javaDetectBtn_clicked() +{ + JavaInstallPtr java; + + VersionSelectDialog vselect(APPLICATION->javalist().get(), tr("Select a Java version"), this, true); + vselect.setResizeOn(2); + vselect.exec(); + + if (vselect.result() == QDialog::Accepted && vselect.selectedVersion()) + { + java = std::dynamic_pointer_cast(vselect.selectedVersion()); + ui->javaPathTextBox->setText(java->path); + } +} + +void JavaPage::on_javaBrowseBtn_clicked() +{ + QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable")); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if(raw_path.isEmpty()) + { + return; + } + + QString cooked_path = FS::NormalizePath(raw_path); + QFileInfo javaInfo(cooked_path);; + if(!javaInfo.exists() || !javaInfo.isExecutable()) + { + return; + } + ui->javaPathTextBox->setText(cooked_path); +} + +void JavaPage::on_javaTestBtn_clicked() +{ + if(checker) + { + return; + } + checker.reset(new JavaCommon::TestCheck( + this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->text(), + ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value())); + connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished())); + checker->run(); +} + +void JavaPage::checkerFinished() +{ + checker.reset(); +} diff --git a/ultimmc/launcher/ui/pages/global/JavaPage.h b/ultimmc/launcher/ui/pages/global/JavaPage.h new file mode 100644 index 0000000..8f9b332 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/JavaPage.h @@ -0,0 +1,72 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include "ui/pages/BasePage.h" +#include "JavaCommon.h" +#include +#include + +class SettingsObject; + +namespace Ui +{ +class JavaPage; +} + +class JavaPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit JavaPage(QWidget *parent = 0); + ~JavaPage(); + + QString displayName() const override + { + return tr("Java"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("java"); + } + QString id() const override + { + return "java-settings"; + } + QString helpPage() const override + { + return "Java-settings"; + } + bool apply() override; + +private: + void applySettings(); + void loadSettings(); + +private +slots: + void on_javaDetectBtn_clicked(); + void on_javaTestBtn_clicked(); + void on_javaBrowseBtn_clicked(); + void checkerFinished(); + +private: + Ui::JavaPage *ui; + unique_qobject_ptr checker; +}; diff --git a/ultimmc/launcher/ui/pages/global/JavaPage.ui b/ultimmc/launcher/ui/pages/global/JavaPage.ui new file mode 100644 index 0000000..b67e999 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/JavaPage.ui @@ -0,0 +1,260 @@ + + + JavaPage + + + + 0 + 0 + 545 + 580 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + Tab 1 + + + + + + Memory + + + + + + The maximum amount of memory Minecraft is allowed to use. + + + MiB + + + 128 + + + 65536 + + + 128 + + + 1024 + + + + + + + Minimum memory allocation: + + + + + + + Maximum memory allocation: + + + + + + + The amount of memory Minecraft is started with. + + + MiB + + + 128 + + + 65536 + + + 128 + + + 256 + + + + + + + PermGen: + + + + + + + The amount of memory available to store loaded Java classes. + + + MiB + + + 64 + + + 999999999 + + + 8 + + + 64 + + + + + + + + + + Java Runtime + + + + + + + 0 + 0 + + + + Java path: + + + + + + + + + + + + + 0 + 0 + + + + + 28 + 16777215 + + + + ... + + + + + + + + + + + + + 0 + 0 + + + + JVM arguments: + + + + + + + + 0 + 0 + + + + Auto-detect... + + + + + + + + 0 + 0 + + + + Test + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + minMemSpinBox + maxMemSpinBox + permGenSpinBox + javaBrowseBtn + javaPathTextBox + jvmArgsTextBox + javaDetectBtn + javaTestBtn + tabWidget + + + + diff --git a/ultimmc/launcher/ui/pages/global/LanguagePage.cpp b/ultimmc/launcher/ui/pages/global/LanguagePage.cpp new file mode 100644 index 0000000..359fdee --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/LanguagePage.cpp @@ -0,0 +1,51 @@ +#include "LanguagePage.h" + +#include "ui/widgets/LanguageSelectionWidget.h" +#include + +LanguagePage::LanguagePage(QWidget* parent) : + QWidget(parent) +{ + setObjectName(QStringLiteral("languagePage")); + auto layout = new QVBoxLayout(this); + mainWidget = new LanguageSelectionWidget(this); + layout->setContentsMargins(0,0,0,0); + layout->addWidget(mainWidget); + retranslate(); +} + +LanguagePage::~LanguagePage() +{ +} + +bool LanguagePage::apply() +{ + applySettings(); + return true; +} + +void LanguagePage::applySettings() +{ + auto settings = APPLICATION->settings(); + QString key = mainWidget->getSelectedLanguageKey(); + settings->set("Language", key); +} + +void LanguagePage::loadSettings() +{ + // NIL +} + +void LanguagePage::retranslate() +{ + mainWidget->retranslate(); +} + +void LanguagePage::changeEvent(QEvent* event) +{ + if (event->type() == QEvent::LanguageChange) + { + retranslate(); + } + QWidget::changeEvent(event); +} diff --git a/ultimmc/launcher/ui/pages/global/LanguagePage.h b/ultimmc/launcher/ui/pages/global/LanguagePage.h new file mode 100644 index 0000000..b1dd05a --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/LanguagePage.h @@ -0,0 +1,60 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "ui/pages/BasePage.h" +#include +#include + +class LanguageSelectionWidget; + +class LanguagePage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit LanguagePage(QWidget *parent = 0); + virtual ~LanguagePage(); + + QString displayName() const override + { + return tr("Language"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("language"); + } + QString id() const override + { + return "language-settings"; + } + QString helpPage() const override + { + return "Language-settings"; + } + bool apply() override; + + void changeEvent(QEvent * ) override; + +private: + void applySettings(); + void loadSettings(); + void retranslate(); + +private: + LanguageSelectionWidget *mainWidget; +}; diff --git a/ultimmc/launcher/ui/pages/global/LauncherPage.cpp b/ultimmc/launcher/ui/pages/global/LauncherPage.cpp new file mode 100644 index 0000000..1f986c1 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/LauncherPage.cpp @@ -0,0 +1,381 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LauncherPage.h" +#include "ui_LauncherPage.h" + +#include +#include +#include +#include + +#include "updater/UpdateChecker.h" + +#include "settings/SettingsObject.h" +#include +#include "Application.h" +#include "BuildConfig.h" +#include "ui/themes/ITheme.h" + +#include +#include + +// FIXME: possibly move elsewhere +enum InstSortMode +{ + // Sort alphabetically by name. + Sort_Name, + // Sort by which instance was launched most recently. + Sort_LastLaunch +}; + +LauncherPage::LauncherPage(QWidget *parent) : QWidget(parent), ui(new Ui::LauncherPage) +{ + ui->setupUi(this); + auto origForeground = ui->fontPreview->palette().color(ui->fontPreview->foregroundRole()); + auto origBackground = ui->fontPreview->palette().color(ui->fontPreview->backgroundRole()); + m_colors.reset(new LogColorCache(origForeground, origBackground)); + + ui->sortingModeGroup->setId(ui->sortByNameBtn, Sort_Name); + ui->sortingModeGroup->setId(ui->sortLastLaunchedBtn, Sort_LastLaunch); + + defaultFormat = new QTextCharFormat(ui->fontPreview->currentCharFormat()); + + m_languageModel = APPLICATION->translations(); + loadSettings(); + + // Updater + if(!BuildConfig.UPDATER_ENABLED) + { + ui->updateSettingsBox->setHidden(true); + } + + // Analytics + if(BuildConfig.ANALYTICS_ID.isEmpty()) + { + ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->analyticsTab)); + } + connect(ui->fontSizeBox, SIGNAL(valueChanged(int)), SLOT(refreshFontPreview())); + connect(ui->consoleFont, SIGNAL(currentFontChanged(QFont)), SLOT(refreshFontPreview())); + + //move mac data button + QFile file(QDir::current().absolutePath() + "/dontmovemacdata"); + if (!file.exists()) + { + ui->migrateDataFolderMacBtn->setVisible(false); + } +} + +LauncherPage::~LauncherPage() +{ + delete ui; + delete defaultFormat; +} + +bool LauncherPage::apply() +{ + applySettings(); + return true; +} + +void LauncherPage::on_instDirBrowseBtn_clicked() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Instance Folder"), ui->instDirTextBox->text()); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) + { + QString cooked_dir = FS::NormalizePath(raw_dir); + if (FS::checkProblemticPathJava(QDir(cooked_dir))) + { + QMessageBox warning; + warning.setText(tr("You're trying to specify an instance folder which\'s path " + "contains at least one \'!\'. " + "Java is known to cause problems if that is the case, your " + "instances (probably) won't start!")); + warning.setInformativeText( + tr("Do you really want to use this path? " + "Selecting \"No\" will close this and not alter your instance path.")); + warning.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + int result = warning.exec(); + if (result == QMessageBox::Yes) + { + ui->instDirTextBox->setText(cooked_dir); + } + } + else + { + ui->instDirTextBox->setText(cooked_dir); + } + } +} + +void LauncherPage::on_iconsDirBrowseBtn_clicked() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Icons Folder"), ui->iconsDirTextBox->text()); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) + { + QString cooked_dir = FS::NormalizePath(raw_dir); + ui->iconsDirTextBox->setText(cooked_dir); + } +} +void LauncherPage::on_modsDirBrowseBtn_clicked() +{ + QString raw_dir = QFileDialog::getExistingDirectory(this, tr("Mods Folder"), ui->modsDirTextBox->text()); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if (!raw_dir.isEmpty() && QDir(raw_dir).exists()) + { + QString cooked_dir = FS::NormalizePath(raw_dir); + ui->modsDirTextBox->setText(cooked_dir); + } +} +void LauncherPage::on_migrateDataFolderMacBtn_clicked() +{ + QFile file(QDir::current().absolutePath() + "/dontmovemacdata"); + file.remove(); + QProcess::startDetached(qApp->arguments()[0]); + qApp->quit(); +} + +void LauncherPage::applySettings() +{ + auto s = APPLICATION->settings(); + + if (ui->resetNotificationsBtn->isChecked()) + { + s->set("ShownNotifications", QString()); + } + + // Updates + s->set("AutoUpdate", ui->autoUpdateCheckBox->isChecked()); + auto original = s->get("IconTheme").toString(); + //FIXME: make generic + switch (ui->themeComboBox->currentIndex()) + { + case 1: + s->set("IconTheme", "pe_dark"); + break; + case 2: + s->set("IconTheme", "pe_light"); + break; + case 3: + s->set("IconTheme", "pe_blue"); + break; + case 4: + s->set("IconTheme", "pe_colored"); + break; + case 5: + s->set("IconTheme", "OSX"); + break; + case 6: + s->set("IconTheme", "iOS"); + break; + case 7: + s->set("IconTheme", "flat"); + break; + case 8: + s->set("IconTheme", "custom"); + break; + case 0: + default: + s->set("IconTheme", "multimc"); + break; + } + + if(original != s->get("IconTheme")) + { + APPLICATION->setIconTheme(s->get("IconTheme").toString()); + } + + auto originalAppTheme = s->get("ApplicationTheme").toString(); + auto newAppTheme = ui->themeComboBoxColors->currentData().toString(); + if(originalAppTheme != newAppTheme) + { + s->set("ApplicationTheme", newAppTheme); + APPLICATION->setApplicationTheme(newAppTheme, false); + } + + // Console settings + s->set("ShowConsole", ui->showConsoleCheck->isChecked()); + s->set("AutoCloseConsole", ui->autoCloseConsoleCheck->isChecked()); + s->set("ShowConsoleOnError", ui->showConsoleErrorCheck->isChecked()); + QString consoleFontFamily = ui->consoleFont->currentFont().family(); + s->set("ConsoleFont", consoleFontFamily); + s->set("ConsoleFontSize", ui->fontSizeBox->value()); + s->set("ConsoleMaxLines", ui->lineLimitSpinBox->value()); + s->set("ConsoleOverflowStop", ui->checkStopLogging->checkState() != Qt::Unchecked); + + // Folders + // TODO: Offer to move instances to new instance folder. + s->set("InstanceDir", ui->instDirTextBox->text()); + s->set("CentralModsDir", ui->modsDirTextBox->text()); + s->set("IconsDir", ui->iconsDirTextBox->text()); + + auto sortMode = (InstSortMode)ui->sortingModeGroup->checkedId(); + switch (sortMode) + { + case Sort_LastLaunch: + s->set("InstSortMode", "LastLaunch"); + break; + case Sort_Name: + default: + s->set("InstSortMode", "Name"); + break; + } + + // Analytics + if(!BuildConfig.ANALYTICS_ID.isEmpty()) + { + s->set("Analytics", ui->analyticsCheck->isChecked()); + } +} +void LauncherPage::loadSettings() +{ + auto s = APPLICATION->settings(); + // Updates + ui->autoUpdateCheckBox->setChecked(s->get("AutoUpdate").toBool()); + //FIXME: make generic + auto theme = s->get("IconTheme").toString(); + if (theme == "pe_dark") + { + ui->themeComboBox->setCurrentIndex(1); + } + else if (theme == "pe_light") + { + ui->themeComboBox->setCurrentIndex(2); + } + else if (theme == "pe_blue") + { + ui->themeComboBox->setCurrentIndex(3); + } + else if (theme == "pe_colored") + { + ui->themeComboBox->setCurrentIndex(4); + } + else if (theme == "OSX") + { + ui->themeComboBox->setCurrentIndex(5); + } + else if (theme == "iOS") + { + ui->themeComboBox->setCurrentIndex(6); + } + else if (theme == "flat") + { + ui->themeComboBox->setCurrentIndex(7); + } + else if (theme == "custom") + { + ui->themeComboBox->setCurrentIndex(8); + } + else + { + ui->themeComboBox->setCurrentIndex(0); + } + + { + auto currentTheme = s->get("ApplicationTheme").toString(); + auto themes = APPLICATION->getValidApplicationThemes(); + int idx = 0; + for(auto &theme: themes) + { + ui->themeComboBoxColors->addItem(theme->name(), theme->id()); + if(currentTheme == theme->id()) + { + ui->themeComboBoxColors->setCurrentIndex(idx); + } + idx++; + } + } + + // Console settings + ui->showConsoleCheck->setChecked(s->get("ShowConsole").toBool()); + ui->autoCloseConsoleCheck->setChecked(s->get("AutoCloseConsole").toBool()); + ui->showConsoleErrorCheck->setChecked(s->get("ShowConsoleOnError").toBool()); + QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); + QFont consoleFont(fontFamily); + ui->consoleFont->setCurrentFont(consoleFont); + + bool conversionOk = true; + int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); + if(!conversionOk) + { + fontSize = 11; + } + ui->fontSizeBox->setValue(fontSize); + refreshFontPreview(); + ui->lineLimitSpinBox->setValue(s->get("ConsoleMaxLines").toInt()); + ui->checkStopLogging->setChecked(s->get("ConsoleOverflowStop").toBool()); + + // Folders + ui->instDirTextBox->setText(s->get("InstanceDir").toString()); + ui->modsDirTextBox->setText(s->get("CentralModsDir").toString()); + ui->iconsDirTextBox->setText(s->get("IconsDir").toString()); + + QString sortMode = s->get("InstSortMode").toString(); + + if (sortMode == "LastLaunch") + { + ui->sortLastLaunchedBtn->setChecked(true); + } + else + { + ui->sortByNameBtn->setChecked(true); + } + + // Analytics + if(!BuildConfig.ANALYTICS_ID.isEmpty()) + { + ui->analyticsCheck->setChecked(s->get("Analytics").toBool()); + } +} + +void LauncherPage::refreshFontPreview() +{ + int fontSize = ui->fontSizeBox->value(); + QString fontFamily = ui->consoleFont->currentFont().family(); + ui->fontPreview->clear(); + defaultFormat->setFont(QFont(fontFamily, fontSize)); + { + QTextCharFormat format(*defaultFormat); + format.setForeground(m_colors->getFront(MessageLevel::Error)); + // append a paragraph/line + auto workCursor = ui->fontPreview->textCursor(); + workCursor.movePosition(QTextCursor::End); + workCursor.insertText(tr("[Something/ERROR] A spooky error!"), format); + workCursor.insertBlock(); + } + { + QTextCharFormat format(*defaultFormat); + format.setForeground(m_colors->getFront(MessageLevel::Message)); + // append a paragraph/line + auto workCursor = ui->fontPreview->textCursor(); + workCursor.movePosition(QTextCursor::End); + workCursor.insertText(tr("[Test/INFO] A harmless message..."), format); + workCursor.insertBlock(); + } + { + QTextCharFormat format(*defaultFormat); + format.setForeground(m_colors->getFront(MessageLevel::Warning)); + // append a paragraph/line + auto workCursor = ui->fontPreview->textCursor(); + workCursor.movePosition(QTextCursor::End); + workCursor.insertText(tr("[Something/WARN] A not so spooky warning."), format); + workCursor.insertBlock(); + } +} diff --git a/ultimmc/launcher/ui/pages/global/LauncherPage.h b/ultimmc/launcher/ui/pages/global/LauncherPage.h new file mode 100644 index 0000000..d5ea235 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/LauncherPage.h @@ -0,0 +1,86 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "java/JavaChecker.h" +#include "ui/pages/BasePage.h" +#include +#include "ui/ColorCache.h" +#include + +class QTextCharFormat; +class SettingsObject; + +namespace Ui +{ +class LauncherPage; +} + +class LauncherPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit LauncherPage(QWidget *parent = 0); + ~LauncherPage(); + + QString displayName() const override + { + return "Launcher"; + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("launcher"); + } + QString id() const override + { + return "launcher-settings"; + } + QString helpPage() const override + { + return "Launcher-settings"; + } + bool apply() override; + +private: + void applySettings(); + void loadSettings(); + +private +slots: + void on_instDirBrowseBtn_clicked(); + void on_modsDirBrowseBtn_clicked(); + void on_iconsDirBrowseBtn_clicked(); + void on_migrateDataFolderMacBtn_clicked(); + + /*! + * Updates the font preview + */ + void refreshFontPreview(); + +private: + Ui::LauncherPage *ui; + + // default format for the font preview... + QTextCharFormat *defaultFormat; + + std::unique_ptr m_colors; + + std::shared_ptr m_languageModel; +}; diff --git a/ultimmc/launcher/ui/pages/global/LauncherPage.ui b/ultimmc/launcher/ui/pages/global/LauncherPage.ui new file mode 100644 index 0000000..d1728fe --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/LauncherPage.ui @@ -0,0 +1,556 @@ + + + LauncherPage + + + + 0 + 0 + 514 + 629 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + QTabWidget::Rounded + + + 0 + + + + Features + + + + + + Update Settings + + + + + + Check for updates on start? + + + + + + + + + + Folders + + + + + + I&nstances: + + + instDirTextBox + + + + + + + + + + ... + + + + + + + &Mods: + + + modsDirTextBox + + + + + + + + + + ... + + + + + + + + + + &Icons: + + + iconsDirTextBox + + + + + + + ... + + + + + + + + + + Move the data to new location (will restart the launcher) + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + User Interface + + + + + + Launcher notifications + + + + + + Reset hidden notifications + + + true + + + + + + + + + + true + + + Instance view sorting mode + + + + + + By &last launched + + + sortingModeGroup + + + + + + + By &name + + + sortingModeGroup + + + + + + + + + + Theme + + + + + + &Icons + + + themeComboBox + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + Default + + + + + Simple (Dark Icons) + + + + + Simple (Light Icons) + + + + + Simple (Blue Icons) + + + + + Simple (Colored Icons) + + + + + OSX + + + + + iOS + + + + + Flat + + + + + Custom + + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + + + + + Colors + + + themeComboBoxColors + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + Console + + + + + + Console Settings + + + + + + Show console while the game is running? + + + + + + + Automatically close console when the game quits? + + + + + + + Show console when the game crashes? + + + + + + + + + + History limit + + + + + + Stop logging when log overflows + + + + + + + + 0 + 0 + + + + lines + + + 10000 + + + 1000000 + + + 10000 + + + 100000 + + + + + + + + + + + 0 + 0 + + + + Console font + + + + + + + 0 + 0 + + + + Qt::ScrollBarAlwaysOff + + + false + + + Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + + + + + 5 + + + 16 + + + 11 + + + + + + + + + + + Analytics + + + + + + Analytics Settings + + + + + + Send anonymous usage statistics? + + + + + + + Qt::Horizontal + + + + + + + <html><head/> +<body> +<p>The launcher sends anonymous usage statistics on every start of the application.</p><p>The following data is collected:</p> +<ul> +<li>Launcher version.</li> +<li>Operating system name, version and architecture.</li> +<li>CPU architecture (kernel architecture on linux).</li> +<li>Size of system memory.</li> +<li>Java version, architecture and memory settings.</li> +</ul> +</body></html> + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + tabWidget + autoUpdateCheckBox + instDirTextBox + instDirBrowseBtn + modsDirTextBox + modsDirBrowseBtn + iconsDirTextBox + iconsDirBrowseBtn + resetNotificationsBtn + sortLastLaunchedBtn + sortByNameBtn + themeComboBox + themeComboBoxColors + showConsoleCheck + autoCloseConsoleCheck + showConsoleErrorCheck + lineLimitSpinBox + checkStopLogging + consoleFont + fontSizeBox + fontPreview + + + + + + + diff --git a/ultimmc/launcher/ui/pages/global/MinecraftPage.cpp b/ultimmc/launcher/ui/pages/global/MinecraftPage.cpp new file mode 100644 index 0000000..8491e98 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/MinecraftPage.cpp @@ -0,0 +1,93 @@ +/* Copyright 2013-2022 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "MinecraftPage.h" +#include "ui_MinecraftPage.h" + +#include +#include +#include + +#include "settings/SettingsObject.h" +#include "Application.h" + +MinecraftPage::MinecraftPage(QWidget *parent) : QWidget(parent), ui(new Ui::MinecraftPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + loadSettings(); + updateCheckboxStuff(); +} + +MinecraftPage::~MinecraftPage() +{ + delete ui; +} + +bool MinecraftPage::apply() +{ + applySettings(); + return true; +} + +void MinecraftPage::updateCheckboxStuff() +{ + ui->windowWidthSpinBox->setEnabled(!ui->maximizedCheckBox->isChecked()); + ui->windowHeightSpinBox->setEnabled(!ui->maximizedCheckBox->isChecked()); +} + +void MinecraftPage::on_maximizedCheckBox_clicked(bool checked) +{ + Q_UNUSED(checked); + updateCheckboxStuff(); +} + +void MinecraftPage::applySettings() +{ + auto s = APPLICATION->settings(); + + // Window Size + s->set("LaunchMaximized", ui->maximizedCheckBox->isChecked()); + s->set("MinecraftWinWidth", ui->windowWidthSpinBox->value()); + s->set("MinecraftWinHeight", ui->windowHeightSpinBox->value()); + + // Native library workarounds + s->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); + s->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked()); + + // Game time + s->set("ShowGameTime", ui->showGameTime->isChecked()); + s->set("ShowGlobalGameTime", ui->showGlobalGameTime->isChecked()); + s->set("RecordGameTime", ui->recordGameTime->isChecked()); + s->set("ShowGameTimeHours", ui->showGameTimeHours->isChecked()); +} + +void MinecraftPage::loadSettings() +{ + auto s = APPLICATION->settings(); + + // Window Size + ui->maximizedCheckBox->setChecked(s->get("LaunchMaximized").toBool()); + ui->windowWidthSpinBox->setValue(s->get("MinecraftWinWidth").toInt()); + ui->windowHeightSpinBox->setValue(s->get("MinecraftWinHeight").toInt()); + + ui->useNativeOpenALCheck->setChecked(s->get("UseNativeOpenAL").toBool()); + ui->useNativeGLFWCheck->setChecked(s->get("UseNativeGLFW").toBool()); + + ui->showGameTime->setChecked(s->get("ShowGameTime").toBool()); + ui->showGlobalGameTime->setChecked(s->get("ShowGlobalGameTime").toBool()); + ui->recordGameTime->setChecked(s->get("RecordGameTime").toBool()); + ui->showGameTimeHours->setChecked(s->get("ShowGameTimeHours").toBool()); +} diff --git a/ultimmc/launcher/ui/pages/global/MinecraftPage.h b/ultimmc/launcher/ui/pages/global/MinecraftPage.h new file mode 100644 index 0000000..42626d9 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/MinecraftPage.h @@ -0,0 +1,70 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "java/JavaChecker.h" +#include "ui/pages/BasePage.h" +#include + +class SettingsObject; + +namespace Ui +{ +class MinecraftPage; +} + +class MinecraftPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit MinecraftPage(QWidget *parent = 0); + ~MinecraftPage(); + + QString displayName() const override + { + return tr("Minecraft"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("minecraft"); + } + QString id() const override + { + return "minecraft-settings"; + } + QString helpPage() const override + { + return "Minecraft-settings"; + } + bool apply() override; + +private: + void updateCheckboxStuff(); + void applySettings(); + void loadSettings(); + +private +slots: + void on_maximizedCheckBox_clicked(bool checked); + +private: + Ui::MinecraftPage *ui; + +}; diff --git a/ultimmc/launcher/ui/pages/global/MinecraftPage.ui b/ultimmc/launcher/ui/pages/global/MinecraftPage.ui new file mode 100644 index 0000000..7a5137d --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/MinecraftPage.ui @@ -0,0 +1,203 @@ + + + MinecraftPage + + + + 0 + 0 + 936 + 1134 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QTabWidget::Rounded + + + 0 + + + + Minecraft + + + + + + Window Size + + + + + + Start Minecraft maximized? + + + + + + + + + Window hei&ght: + + + windowHeightSpinBox + + + + + + + W&indow width: + + + windowWidthSpinBox + + + + + + + 1 + + + 65536 + + + 1 + + + 854 + + + + + + + 1 + + + 65536 + + + 480 + + + + + + + + + + + + Native library workarounds + + + + + + Use system installation of GLFW + + + + + + + Use system installation of OpenAL + + + + + + + + + + Game time + + + + + + Show time spent playing instances + + + + + + + Show time spent playing across all instances + + + + + + + Record time spent playing instances + + + + + + + Show time spent playing in hours only + + + + + + + + + + Qt::Vertical + + + + 0 + 0 + + + + + + + + + + + + tabWidget + maximizedCheckBox + windowWidthSpinBox + windowHeightSpinBox + useNativeGLFWCheck + useNativeOpenALCheck + + + + diff --git a/ultimmc/launcher/ui/pages/global/PasteEEPage.cpp b/ultimmc/launcher/ui/pages/global/PasteEEPage.cpp new file mode 100644 index 0000000..4b375d9 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/PasteEEPage.cpp @@ -0,0 +1,81 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PasteEEPage.h" +#include "ui_PasteEEPage.h" + +#include +#include +#include +#include + +#include "settings/SettingsObject.h" +#include "tools/BaseProfiler.h" +#include "Application.h" + +PasteEEPage::PasteEEPage(QWidget *parent) : + QWidget(parent), + ui(new Ui::PasteEEPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide();\ + connect(ui->customAPIkeyEdit, &QLineEdit::textEdited, this, &PasteEEPage::textEdited); + loadSettings(); +} + +PasteEEPage::~PasteEEPage() +{ + delete ui; +} + +void PasteEEPage::loadSettings() +{ + auto s = APPLICATION->settings(); + QString keyToUse = s->get("PasteEEAPIKey").toString(); + if(keyToUse == "multimc") + { + ui->multimcButton->setChecked(true); + } + else + { + ui->customButton->setChecked(true); + ui->customAPIkeyEdit->setText(keyToUse); + } +} + +void PasteEEPage::applySettings() +{ + auto s = APPLICATION->settings(); + + QString pasteKeyToUse; + if (ui->customButton->isChecked()) + pasteKeyToUse = ui->customAPIkeyEdit->text(); + else + { + pasteKeyToUse = "multimc"; + } + s->set("PasteEEAPIKey", pasteKeyToUse); +} + +bool PasteEEPage::apply() +{ + applySettings(); + return true; +} + +void PasteEEPage::textEdited(const QString& text) +{ + ui->customButton->setChecked(true); +} diff --git a/ultimmc/launcher/ui/pages/global/PasteEEPage.h b/ultimmc/launcher/ui/pages/global/PasteEEPage.h new file mode 100644 index 0000000..a1c7d43 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/PasteEEPage.h @@ -0,0 +1,62 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "ui/pages/BasePage.h" +#include + +namespace Ui { +class PasteEEPage; +} + +class PasteEEPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit PasteEEPage(QWidget *parent = 0); + ~PasteEEPage(); + + QString displayName() const override + { + return tr("Log Upload"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("log"); + } + QString id() const override + { + return "log-upload"; + } + QString helpPage() const override + { + return "Log-Upload"; + } + virtual bool apply() override; + +private: + void loadSettings(); + void applySettings(); + +private slots: + void textEdited(const QString &text); + +private: + Ui::PasteEEPage *ui; +}; diff --git a/ultimmc/launcher/ui/pages/global/PasteEEPage.ui b/ultimmc/launcher/ui/pages/global/PasteEEPage.ui new file mode 100644 index 0000000..1088378 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/PasteEEPage.ui @@ -0,0 +1,128 @@ + + + PasteEEPage + + + + 0 + 0 + 491 + 474 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + Tab 1 + + + + + + paste.ee API key + + + + + + MultiMC key - 12MB &upload limit + + + pasteButtonGroup + + + + + + + &Your own key - 12MB upload limit: + + + pasteButtonGroup + + + + + + + QLineEdit::Password + + + Paste your API key here! + + + + + + + Qt::Horizontal + + + + + + + <html><head/><body><p><a href="https://paste.ee">paste.ee</a> is used by MultiMC for log uploads. If you have a <a href="https://paste.ee">paste.ee</a> account, you can add your API key here and have your uploaded logs paired with your account.</p></body></html> + + + Qt::RichText + + + true + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 216 + + + + + + + + + + + + tabWidget + multimcButton + customButton + customAPIkeyEdit + + + + + + + diff --git a/ultimmc/launcher/ui/pages/global/ProxyPage.cpp b/ultimmc/launcher/ui/pages/global/ProxyPage.cpp new file mode 100644 index 0000000..5bc8199 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/ProxyPage.cpp @@ -0,0 +1,106 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ProxyPage.h" +#include "ui_ProxyPage.h" + +#include + +#include "settings/SettingsObject.h" +#include "Application.h" +#include "Application.h" + +ProxyPage::ProxyPage(QWidget *parent) : QWidget(parent), ui(new Ui::ProxyPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + loadSettings(); + updateCheckboxStuff(); + + connect(ui->proxyGroup, SIGNAL(buttonClicked(int)), SLOT(proxyChanged(int))); +} + +ProxyPage::~ProxyPage() +{ + delete ui; +} + +bool ProxyPage::apply() +{ + applySettings(); + return true; +} + +void ProxyPage::updateCheckboxStuff() +{ + ui->proxyAddrBox->setEnabled(!ui->proxyNoneBtn->isChecked() && + !ui->proxyDefaultBtn->isChecked()); + ui->proxyAuthBox->setEnabled(!ui->proxyNoneBtn->isChecked() && + !ui->proxyDefaultBtn->isChecked()); +} + +void ProxyPage::proxyChanged(int) +{ + updateCheckboxStuff(); +} + +void ProxyPage::applySettings() +{ + auto s = APPLICATION->settings(); + + // Proxy + QString proxyType = "None"; + if (ui->proxyDefaultBtn->isChecked()) + proxyType = "Default"; + else if (ui->proxyNoneBtn->isChecked()) + proxyType = "None"; + else if (ui->proxySOCKS5Btn->isChecked()) + proxyType = "SOCKS5"; + else if (ui->proxyHTTPBtn->isChecked()) + proxyType = "HTTP"; + + s->set("ProxyType", proxyType); + s->set("ProxyAddr", ui->proxyAddrEdit->text()); + s->set("ProxyPort", ui->proxyPortEdit->value()); + s->set("ProxyUser", ui->proxyUserEdit->text()); + s->set("ProxyPass", ui->proxyPassEdit->text()); + + APPLICATION->updateProxySettings( + proxyType, + ui->proxyAddrEdit->text(), + ui->proxyPortEdit->value(), + ui->proxyUserEdit->text(), + ui->proxyPassEdit->text() + ); +} +void ProxyPage::loadSettings() +{ + auto s = APPLICATION->settings(); + // Proxy + QString proxyType = s->get("ProxyType").toString(); + if (proxyType == "Default") + ui->proxyDefaultBtn->setChecked(true); + else if (proxyType == "None") + ui->proxyNoneBtn->setChecked(true); + else if (proxyType == "SOCKS5") + ui->proxySOCKS5Btn->setChecked(true); + else if (proxyType == "HTTP") + ui->proxyHTTPBtn->setChecked(true); + + ui->proxyAddrEdit->setText(s->get("ProxyAddr").toString()); + ui->proxyPortEdit->setValue(s->get("ProxyPort").value()); + ui->proxyUserEdit->setText(s->get("ProxyUser").toString()); + ui->proxyPassEdit->setText(s->get("ProxyPass").toString()); +} diff --git a/ultimmc/launcher/ui/pages/global/ProxyPage.h b/ultimmc/launcher/ui/pages/global/ProxyPage.h new file mode 100644 index 0000000..6698c34 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/ProxyPage.h @@ -0,0 +1,66 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "ui/pages/BasePage.h" +#include + +namespace Ui +{ +class ProxyPage; +} + +class ProxyPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit ProxyPage(QWidget *parent = 0); + ~ProxyPage(); + + QString displayName() const override + { + return tr("Proxy"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("proxy"); + } + QString id() const override + { + return "proxy-settings"; + } + QString helpPage() const override + { + return "Proxy-settings"; + } + bool apply() override; + +private: + void updateCheckboxStuff(); + void applySettings(); + void loadSettings(); + +private +slots: + void proxyChanged(int); + +private: + Ui::ProxyPage *ui; +}; diff --git a/ultimmc/launcher/ui/pages/global/ProxyPage.ui b/ultimmc/launcher/ui/pages/global/ProxyPage.ui new file mode 100644 index 0000000..347fa86 --- /dev/null +++ b/ultimmc/launcher/ui/pages/global/ProxyPage.ui @@ -0,0 +1,203 @@ + + + ProxyPage + + + + 0 + 0 + 598 + 617 + + + + + 0 + 0 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + + This only applies to the launcher. Minecraft does not accept proxy settings. + + + Qt::AlignCenter + + + true + + + + + + + Type + + + + + + Uses your system's default proxy settings. + + + &Default + + + proxyGroup + + + + + + + &None + + + proxyGroup + + + + + + + SOC&KS5 + + + proxyGroup + + + + + + + H&TTP + + + proxyGroup + + + + + + + + + + Address and Port + + + + + + 127.0.0.1 + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + QAbstractSpinBox::PlusMinus + + + 65535 + + + 8080 + + + + + + + + + + Authentication + + + + + + + + + Username: + + + + + + + Password: + + + + + + + QLineEdit::Password + + + + + + + Note: Proxy username and password are stored in plain text inside the launcher's configuration file! + + + true + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + diff --git a/ultimmc/launcher/ui/pages/instance/GameOptionsPage.cpp b/ultimmc/launcher/ui/pages/instance/GameOptionsPage.cpp new file mode 100644 index 0000000..782f2ab --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/GameOptionsPage.cpp @@ -0,0 +1,37 @@ +#include "GameOptionsPage.h" +#include "ui_GameOptionsPage.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/gameoptions/GameOptions.h" + +GameOptionsPage::GameOptionsPage(MinecraftInstance * inst, QWidget* parent) + : QWidget(parent), ui(new Ui::GameOptionsPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + m_model = inst->gameOptionsModel(); + ui->optionsView->setModel(m_model.get()); + auto head = ui->optionsView->header(); + if(head->count()) + { + head->setSectionResizeMode(0, QHeaderView::ResizeToContents); + for(int i = 1; i < head->count(); i++) + { + head->setSectionResizeMode(i, QHeaderView::Stretch); + } + } +} + +GameOptionsPage::~GameOptionsPage() +{ + // m_model->save(); +} + +void GameOptionsPage::openedImpl() +{ + // m_model->observe(); +} + +void GameOptionsPage::closedImpl() +{ + // m_model->unobserve(); +} diff --git a/ultimmc/launcher/ui/pages/instance/GameOptionsPage.h b/ultimmc/launcher/ui/pages/instance/GameOptionsPage.h new file mode 100644 index 0000000..878903e --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/GameOptionsPage.h @@ -0,0 +1,63 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "ui/pages/BasePage.h" +#include + +namespace Ui +{ +class GameOptionsPage; +} + +class GameOptions; +class MinecraftInstance; + +class GameOptionsPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit GameOptionsPage(MinecraftInstance *inst, QWidget *parent = 0); + virtual ~GameOptionsPage(); + + void openedImpl() override; + void closedImpl() override; + + virtual QString displayName() const override + { + return tr("Game Options"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("settings"); + } + virtual QString id() const override + { + return "gameoptions"; + } + virtual QString helpPage() const override + { + return "Game-Options-management"; + } + +private: // data + Ui::GameOptionsPage *ui = nullptr; + std::shared_ptr m_model; +}; diff --git a/ultimmc/launcher/ui/pages/instance/GameOptionsPage.ui b/ultimmc/launcher/ui/pages/instance/GameOptionsPage.ui new file mode 100644 index 0000000..f0a5ce0 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/GameOptionsPage.ui @@ -0,0 +1,88 @@ + + + GameOptionsPage + + + + 0 + 0 + 706 + 575 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + 0 + 0 + + + + Tab 1 + + + + + + + 0 + 0 + + + + true + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + 64 + 64 + + + + false + + + false + + + + + + + + + + + tabWidget + optionsView + + + + diff --git a/ultimmc/launcher/ui/pages/instance/InstanceSettingsPage.cpp b/ultimmc/launcher/ui/pages/instance/InstanceSettingsPage.cpp new file mode 100644 index 0000000..36e837d --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/InstanceSettingsPage.cpp @@ -0,0 +1,402 @@ +#include "InstanceSettingsPage.h" +#include "ui_InstanceSettingsPage.h" + +#include +#include +#include + +#include + +#include "ui/dialogs/VersionSelectDialog.h" +#include "ui/widgets/CustomCommands.h" + +#include "JavaCommon.h" +#include "Application.h" + +#include "java/JavaInstallList.h" +#include "FileSystem.h" + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "minecraft/VersionFilterData.h" +#include "minecraft/WorldList.h" + +InstanceSettingsPage::InstanceSettingsPage(BaseInstance *inst, QWidget *parent) + : QWidget(parent), ui(new Ui::InstanceSettingsPage), m_instance(inst) +{ + m_settings = inst->settings(); + ui->setupUi(this); + auto sysMB = Sys::getSystemRam() / Sys::mebibyte; + ui->maxMemSpinBox->setMaximum(sysMB); + connect(ui->openGlobalJavaSettingsButton, &QCommandLinkButton::clicked, this, &InstanceSettingsPage::globalSettingsButtonClicked); + connect(APPLICATION, &Application::globalSettingsAboutToOpen, this, &InstanceSettingsPage::applySettings); + connect(APPLICATION, &Application::globalSettingsClosed, this, &InstanceSettingsPage::loadSettings); + + auto *mcInst = dynamic_cast(inst); + if (mcInst && mcInst->getPackProfile()->getComponent("net.minecraft")->getReleaseDateTime() >= g_VersionFilterData.quickPlayBeginsDate) + { + mcInst->worldList()->update(); + for (const auto &world : mcInst->worldList()->allWorlds()) + { + ui->worldsComboBox->addItem(world.folderName()); + } + } + else + { + ui->worldRadioButton->setVisible(false); + ui->worldsComboBox->setVisible(false); + ui->serverAddressRadioButton->setChecked(true); + } + + loadSettings(); +} + +bool InstanceSettingsPage::shouldDisplay() const +{ + return !m_instance->isRunning(); +} + +InstanceSettingsPage::~InstanceSettingsPage() +{ + delete ui; +} + +void InstanceSettingsPage::globalSettingsButtonClicked(bool) +{ + switch(ui->settingsTabs->currentIndex()) { + case 0: + APPLICATION->ShowGlobalSettings(this, "java-settings"); + return; + case 1: + APPLICATION->ShowGlobalSettings(this, "minecraft-settings"); + return; + case 2: + APPLICATION->ShowGlobalSettings(this, "custom-commands"); + return; + } +} + +bool InstanceSettingsPage::apply() +{ + applySettings(); + return true; +} + +void InstanceSettingsPage::applySettings() +{ + SettingsObject::Lock lock(m_settings); + + // Console + bool console = ui->consoleSettingsBox->isChecked(); + m_settings->set("OverrideConsole", console); + if (console) + { + m_settings->set("ShowConsole", ui->showConsoleCheck->isChecked()); + m_settings->set("AutoCloseConsole", ui->autoCloseConsoleCheck->isChecked()); + m_settings->set("ShowConsoleOnError", ui->showConsoleErrorCheck->isChecked()); + } + else + { + m_settings->reset("ShowConsole"); + m_settings->reset("AutoCloseConsole"); + m_settings->reset("ShowConsoleOnError"); + } + + // Window Size + bool window = ui->windowSizeGroupBox->isChecked(); + m_settings->set("OverrideWindow", window); + if (window) + { + m_settings->set("LaunchMaximized", ui->maximizedCheckBox->isChecked()); + m_settings->set("MinecraftWinWidth", ui->windowWidthSpinBox->value()); + m_settings->set("MinecraftWinHeight", ui->windowHeightSpinBox->value()); + } + else + { + m_settings->reset("LaunchMaximized"); + m_settings->reset("MinecraftWinWidth"); + m_settings->reset("MinecraftWinHeight"); + } + + // Memory + bool memory = ui->memoryGroupBox->isChecked(); + m_settings->set("OverrideMemory", memory); + if (memory) + { + int min = ui->minMemSpinBox->value(); + int max = ui->maxMemSpinBox->value(); + if(min < max) + { + m_settings->set("MinMemAlloc", min); + m_settings->set("MaxMemAlloc", max); + } + else + { + m_settings->set("MinMemAlloc", max); + m_settings->set("MaxMemAlloc", min); + } + m_settings->set("PermGen", ui->permGenSpinBox->value()); + } + else + { + m_settings->reset("MinMemAlloc"); + m_settings->reset("MaxMemAlloc"); + m_settings->reset("PermGen"); + } + + // Java Install Settings + bool javaInstall = ui->javaSettingsGroupBox->isChecked(); + m_settings->set("OverrideJavaLocation", javaInstall); + if (javaInstall) + { + m_settings->set("JavaPath", ui->javaPathTextBox->text()); + } + else + { + m_settings->reset("JavaPath"); + } + + // Java arguments + bool javaArgs = ui->javaArgumentsGroupBox->isChecked(); + m_settings->set("OverrideJavaArgs", javaArgs); + if(javaArgs) + { + m_settings->set("JvmArgs", ui->jvmArgsTextBox->toPlainText().replace("\n", " ")); + } + else + { + m_settings->reset("JvmArgs"); + } + + // old generic 'override both' is removed. + m_settings->reset("OverrideJava"); + + // Custom Commands + bool custcmd = ui->customCommands->checked(); + m_settings->set("OverrideCommands", custcmd); + if (custcmd) + { + m_settings->set("PreLaunchCommand", ui->customCommands->prelaunchCommand()); + m_settings->set("WrapperCommand", ui->customCommands->wrapperCommand()); + m_settings->set("PostExitCommand", ui->customCommands->postexitCommand()); + } + else + { + m_settings->reset("PreLaunchCommand"); + m_settings->reset("WrapperCommand"); + m_settings->reset("PostExitCommand"); + } + + // Workarounds + bool workarounds = ui->nativeWorkaroundsGroupBox->isChecked(); + m_settings->set("OverrideNativeWorkarounds", workarounds); + if(workarounds) + { + m_settings->set("UseNativeOpenAL", ui->useNativeOpenALCheck->isChecked()); + m_settings->set("UseNativeGLFW", ui->useNativeGLFWCheck->isChecked()); + } + else + { + m_settings->reset("UseNativeOpenAL"); + m_settings->reset("UseNativeGLFW"); + } + + // Game time + bool gameTime = ui->gameTimeGroupBox->isChecked(); + m_settings->set("OverrideGameTime", gameTime); + if (gameTime) + { + m_settings->set("ShowGameTime", ui->showGameTime->isChecked()); + m_settings->set("RecordGameTime", ui->recordGameTime->isChecked()); + } + else + { + m_settings->reset("ShowGameTime"); + m_settings->reset("RecordGameTime"); + } + + // Join server on launch + bool joinWorldOnLaunch = ui->quickPlayGroupBox->isChecked(); + m_settings->set("JoinWorldOnLaunch", joinWorldOnLaunch); + + bool joinServerOnLaunch = ui->serverAddressRadioButton->isChecked(); + m_settings->set("JoinServerOnLaunch", joinServerOnLaunch); + + if (joinServerOnLaunch) + { + m_settings->set("JoinServerOnLaunchAddress", ui->serverJoinAddress->text()); + } + else + { + m_settings->reset("JoinServerOnLaunchAddress"); + } + + bool joinSingleplayerWorldOnLaunch = ui->worldRadioButton->isChecked(); + m_settings->set("JoinSingleplayerWorldOnLaunch", joinSingleplayerWorldOnLaunch); + + if (joinSingleplayerWorldOnLaunch) + { + m_settings->set("JoinSingleplayerWorldOnLaunchName", ui->worldsComboBox->currentText()); + } + else + { + m_settings->reset("JoinSingleplayerWorldOnLaunchName"); + } +} + +void InstanceSettingsPage::loadSettings() +{ + // Console + ui->consoleSettingsBox->setChecked(m_settings->get("OverrideConsole").toBool()); + ui->showConsoleCheck->setChecked(m_settings->get("ShowConsole").toBool()); + ui->autoCloseConsoleCheck->setChecked(m_settings->get("AutoCloseConsole").toBool()); + ui->showConsoleErrorCheck->setChecked(m_settings->get("ShowConsoleOnError").toBool()); + + // Window Size + ui->windowSizeGroupBox->setChecked(m_settings->get("OverrideWindow").toBool()); + ui->maximizedCheckBox->setChecked(m_settings->get("LaunchMaximized").toBool()); + ui->windowWidthSpinBox->setValue(m_settings->get("MinecraftWinWidth").toInt()); + ui->windowHeightSpinBox->setValue(m_settings->get("MinecraftWinHeight").toInt()); + + // Memory + ui->memoryGroupBox->setChecked(m_settings->get("OverrideMemory").toBool()); + int min = m_settings->get("MinMemAlloc").toInt(); + int max = m_settings->get("MaxMemAlloc").toInt(); + if(min < max) + { + ui->minMemSpinBox->setValue(min); + ui->maxMemSpinBox->setValue(max); + } + else + { + ui->minMemSpinBox->setValue(max); + ui->maxMemSpinBox->setValue(min); + } + ui->permGenSpinBox->setValue(m_settings->get("PermGen").toInt()); + bool permGenVisible = m_settings->get("PermGenVisible").toBool(); + ui->permGenSpinBox->setVisible(permGenVisible); + ui->labelPermGen->setVisible(permGenVisible); + ui->labelPermgenNote->setVisible(permGenVisible); + + + // Java Settings + bool overrideJava = m_settings->get("OverrideJava").toBool(); + bool overrideLocation = m_settings->get("OverrideJavaLocation").toBool() || overrideJava; + bool overrideArgs = m_settings->get("OverrideJavaArgs").toBool() || overrideJava; + + ui->javaSettingsGroupBox->setChecked(overrideLocation); + ui->javaPathTextBox->setText(m_settings->get("JavaPath").toString()); + + ui->javaArgumentsGroupBox->setChecked(overrideArgs); + ui->jvmArgsTextBox->setPlainText(m_settings->get("JvmArgs").toString()); + + // Custom commands + ui->customCommands->initialize( + true, + m_settings->get("OverrideCommands").toBool(), + m_settings->get("PreLaunchCommand").toString(), + m_settings->get("WrapperCommand").toString(), + m_settings->get("PostExitCommand").toString() + ); + + // Workarounds + ui->nativeWorkaroundsGroupBox->setChecked(m_settings->get("OverrideNativeWorkarounds").toBool()); + ui->useNativeGLFWCheck->setChecked(m_settings->get("UseNativeGLFW").toBool()); + ui->useNativeOpenALCheck->setChecked(m_settings->get("UseNativeOpenAL").toBool()); + + // Miscellanous + ui->gameTimeGroupBox->setChecked(m_settings->get("OverrideGameTime").toBool()); + ui->showGameTime->setChecked(m_settings->get("ShowGameTime").toBool()); + ui->recordGameTime->setChecked(m_settings->get("RecordGameTime").toBool()); + + if (!m_settings->contains("JoinWorldOnLaunch")) + { + ui->quickPlayGroupBox->setChecked(m_settings->get("JoinServerOnLaunch").toBool()); + ui->serverAddressRadioButton->setChecked(m_settings->get("JoinServerOnLaunch").toBool()); + ui->worldRadioButton->setChecked(false); + } + else + { + ui->quickPlayGroupBox->setChecked(m_settings->get("JoinWorldOnLaunch").toBool()); + ui->serverAddressRadioButton->setChecked(m_settings->get("JoinServerOnLaunch").toBool()); + ui->serverJoinAddress->setEnabled(m_settings->get("JoinServerOnLaunch").toBool()); + ui->serverJoinAddress->setText(m_settings->get("JoinServerOnLaunchAddress").toString()); + ui->worldRadioButton->setChecked(m_settings->get("JoinSingleplayerWorldOnLaunch").toBool()); + ui->worldsComboBox->setEnabled(m_settings->get("JoinSingleplayerWorldOnLaunch").toBool()); + ui->worldsComboBox->setCurrentText(m_settings->get("JoinSingleplayerWorldOnLaunchName").toString()); + } + +} + +void InstanceSettingsPage::on_javaDetectBtn_clicked() +{ + JavaInstallPtr java; + + VersionSelectDialog vselect(APPLICATION->javalist().get(), tr("Select a Java version"), this, true); + vselect.setResizeOn(2); + vselect.exec(); + + if (vselect.result() == QDialog::Accepted && vselect.selectedVersion()) + { + java = std::dynamic_pointer_cast(vselect.selectedVersion()); + ui->javaPathTextBox->setText(java->path); + bool visible = java->id.requiresPermGen() && m_settings->get("OverrideMemory").toBool(); + ui->permGenSpinBox->setVisible(visible); + ui->labelPermGen->setVisible(visible); + ui->labelPermgenNote->setVisible(visible); + m_settings->set("PermGenVisible", visible); + } +} + +void InstanceSettingsPage::on_javaBrowseBtn_clicked() +{ + QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable")); + + // do not allow current dir - it's dirty. Do not allow dirs that don't exist + if(raw_path.isEmpty()) + { + return; + } + QString cooked_path = FS::NormalizePath(raw_path); + + QFileInfo javaInfo(cooked_path); + if(!javaInfo.exists() || !javaInfo.isExecutable()) + { + return; + } + ui->javaPathTextBox->setText(cooked_path); + + // custom Java could be anything... enable perm gen option + ui->permGenSpinBox->setVisible(true); + ui->labelPermGen->setVisible(true); + ui->labelPermgenNote->setVisible(true); + m_settings->set("PermGenVisible", true); +} + +void InstanceSettingsPage::on_javaTestBtn_clicked() +{ + if(checker) + { + return; + } + checker.reset(new JavaCommon::TestCheck( + this, ui->javaPathTextBox->text(), ui->jvmArgsTextBox->toPlainText().replace("\n", " "), + ui->minMemSpinBox->value(), ui->maxMemSpinBox->value(), ui->permGenSpinBox->value())); + connect(checker.get(), SIGNAL(finished()), SLOT(checkerFinished())); + checker->run(); +} + +void InstanceSettingsPage::on_serverAddressRadioButton_toggled(bool checked) +{ + ui->serverJoinAddress->setEnabled(checked); +} + +void InstanceSettingsPage::on_worldRadioButton_toggled(bool checked) +{ + ui->worldsComboBox->setEnabled(checked); +} + +void InstanceSettingsPage::checkerFinished() +{ + checker.reset(); +} diff --git a/ultimmc/launcher/ui/pages/instance/InstanceSettingsPage.h b/ultimmc/launcher/ui/pages/instance/InstanceSettingsPage.h new file mode 100644 index 0000000..03caeef --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/InstanceSettingsPage.h @@ -0,0 +1,79 @@ +/* Copyright 2013-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "java/JavaChecker.h" +#include "BaseInstance.h" +#include +#include "ui/pages/BasePage.h" +#include "JavaCommon.h" +#include "minecraft/WorldList.h" +#include "Application.h" + +class JavaChecker; +namespace Ui +{ +class InstanceSettingsPage; +} + +class InstanceSettingsPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit InstanceSettingsPage(BaseInstance *inst, QWidget *parent = 0); + virtual ~InstanceSettingsPage(); + virtual QString displayName() const override + { + return tr("Settings"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("instance-settings"); + } + virtual QString id() const override + { + return "settings"; + } + virtual bool apply() override; + virtual QString helpPage() const override + { + return "Instance-settings"; + } + virtual bool shouldDisplay() const override; + +private slots: + void on_javaDetectBtn_clicked(); + void on_javaTestBtn_clicked(); + void on_javaBrowseBtn_clicked(); + void on_serverAddressRadioButton_toggled(bool checked); + void on_worldRadioButton_toggled(bool checked); + + void applySettings(); + void loadSettings(); + + void checkerFinished(); + + void globalSettingsButtonClicked(bool checked); + +private: + Ui::InstanceSettingsPage *ui; + BaseInstance *m_instance; + SettingsObjectPtr m_settings; + unique_qobject_ptr checker; +}; diff --git a/ultimmc/launcher/ui/pages/instance/InstanceSettingsPage.ui b/ultimmc/launcher/ui/pages/instance/InstanceSettingsPage.ui new file mode 100644 index 0000000..a5a9e41 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/InstanceSettingsPage.ui @@ -0,0 +1,552 @@ + + + InstanceSettingsPage + + + + 0 + 0 + 691 + 581 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Open Global Settings + + + The settings here are overrides for global settings. + + + + + + + QTabWidget::Rounded + + + 0 + + + + Java + + + + + + true + + + Java insta&llation + + + true + + + false + + + + + + + + + Auto-detect... + + + + + + + Browse... + + + + + + + Test + + + + + + + + + + true + + + Memor&y + + + true + + + false + + + + + + Minimum memory allocation: + + + + + + + The maximum amount of memory Minecraft is allowed to use. + + + MiB + + + 128 + + + 65536 + + + 128 + + + 1024 + + + + + + + The amount of memory Minecraft is started with. + + + MiB + + + 128 + + + 65536 + + + 128 + + + 256 + + + + + + + The amount of memory available to store loaded Java classes. + + + MiB + + + 64 + + + 999999999 + + + 8 + + + 64 + + + + + + + PermGen: + + + + + + + Maximum memory allocation: + + + + + + + Note: Permgen is set automatically by Java 8 and later + + + + + + + + + + true + + + Java argumen&ts + + + true + + + false + + + + + + + + + + + + + Game windows + + + + + + true + + + Game Window + + + true + + + false + + + + + + Start Minecraft maximized? + + + + + + + + + Window height: + + + + + + + Window width: + + + + + + + 1 + + + 65536 + + + 1 + + + 854 + + + + + + + 1 + + + 65536 + + + 480 + + + + + + + + + + + + true + + + Conso&le Settings + + + true + + + false + + + + + + Show console while the game is running? + + + + + + + Automatically close console when the game quits? + + + + + + + Show console when the game crashes? + + + + + + + + + + Qt::Vertical + + + + 88 + 125 + + + + + + + + + Custom commands + + + + + + + + + + Workarounds + + + + + + true + + + Native libraries + + + true + + + false + + + + + + Use system installation of GLFW + + + + + + + Use system installation of OpenAL + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Miscellaneous + + + + + + true + + + Override global game time settings + + + true + + + false + + + + + + Show time spent playing this instance + + + + + + + Record time spent playing this instance + + + + + + + + + + Set a world to join on launch + + + true + + + false + + + + + + + + Server address: + + + + + + + + + + Singleplayer world: + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + CustomCommands + QWidget +
ui/widgets/CustomCommands.h
+ 1 +
+
+ + openGlobalJavaSettingsButton + settingsTabs + javaSettingsGroupBox + javaPathTextBox + javaDetectBtn + javaBrowseBtn + javaTestBtn + memoryGroupBox + minMemSpinBox + maxMemSpinBox + permGenSpinBox + javaArgumentsGroupBox + jvmArgsTextBox + windowSizeGroupBox + maximizedCheckBox + windowWidthSpinBox + windowHeightSpinBox + consoleSettingsBox + showConsoleCheck + autoCloseConsoleCheck + showConsoleErrorCheck + nativeWorkaroundsGroupBox + useNativeGLFWCheck + useNativeOpenALCheck + showGameTime + recordGameTime + + + +
diff --git a/ultimmc/launcher/ui/pages/instance/LegacyUpgradePage.cpp b/ultimmc/launcher/ui/pages/instance/LegacyUpgradePage.cpp new file mode 100644 index 0000000..cb78af0 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/LegacyUpgradePage.cpp @@ -0,0 +1,51 @@ +#include "LegacyUpgradePage.h" +#include "ui_LegacyUpgradePage.h" + +#include "InstanceList.h" +#include "minecraft/legacy/LegacyInstance.h" +#include "minecraft/legacy/LegacyUpgradeTask.h" +#include "Application.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/ProgressDialog.h" + +LegacyUpgradePage::LegacyUpgradePage(InstancePtr inst, QWidget *parent) + : QWidget(parent), ui(new Ui::LegacyUpgradePage), m_inst(inst) +{ + ui->setupUi(this); +} + +LegacyUpgradePage::~LegacyUpgradePage() +{ + delete ui; +} + +void LegacyUpgradePage::runModalTask(Task *task) +{ + connect(task, &Task::failed, [this](QString reason) + { + CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Warning)->show(); + }); + ProgressDialog loadDialog(this); + loadDialog.setSkipButton(true, tr("Abort")); + if(loadDialog.execWithTask(task) == QDialog::Accepted) + { + m_container->requestClose(); + } +} + +void LegacyUpgradePage::on_upgradeButton_clicked() +{ + QString newName = tr("%1 (Migrated)").arg(m_inst->name()); + auto upgradeTask = new LegacyUpgradeTask(m_inst); + upgradeTask->setName(newName); + upgradeTask->setGroup(APPLICATION->instances()->getInstanceGroup(m_inst->id())); + upgradeTask->setIcon(m_inst->iconKey()); + unique_qobject_ptr task(APPLICATION->instances()->wrapInstanceTask(upgradeTask)); + runModalTask(task.get()); +} + +bool LegacyUpgradePage::shouldDisplay() const +{ + return !m_inst->isRunning(); +} diff --git a/ultimmc/launcher/ui/pages/instance/LegacyUpgradePage.h b/ultimmc/launcher/ui/pages/instance/LegacyUpgradePage.h new file mode 100644 index 0000000..7c51956 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/LegacyUpgradePage.h @@ -0,0 +1,64 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "minecraft/legacy/LegacyInstance.h" +#include "ui/pages/BasePage.h" +#include +#include "tasks/Task.h" + +namespace Ui +{ +class LegacyUpgradePage; +} + +class LegacyUpgradePage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit LegacyUpgradePage(InstancePtr inst, QWidget *parent = 0); + virtual ~LegacyUpgradePage(); + virtual QString displayName() const override + { + return tr("Upgrade"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("checkupdate"); + } + virtual QString id() const override + { + return "upgrade"; + } + virtual QString helpPage() const override + { + return "Legacy-upgrade"; + } + virtual bool shouldDisplay() const override; + +private slots: + void on_upgradeButton_clicked(); + +private: + void runModalTask(Task *task); + +private: + Ui::LegacyUpgradePage *ui; + InstancePtr m_inst; +}; diff --git a/ultimmc/launcher/ui/pages/instance/LegacyUpgradePage.ui b/ultimmc/launcher/ui/pages/instance/LegacyUpgradePage.ui new file mode 100644 index 0000000..085919e --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/LegacyUpgradePage.ui @@ -0,0 +1,47 @@ + + + LegacyUpgradePage + + + + 0 + 0 + 546 + 405 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + <html><body><h1>Upgrade is required</h1><p>MultiMC now supports old Minecraft versions and all the required features in the new (OneSix) instance format. As a consequence, the old (Legacy) format has been entirely disabled and old instances need to be upgraded.</p><p>The upgrade will create a new instance with the same contents as the current one, in the new format. The original instance will remain untouched, in case anything goes wrong in the process.</p><p>Please report any issues on our <a href="https://github.com/MultiMC/Launcher/issues">github issues page</a>.</p><p>There is also a <a href="https://discord.gg/GtPmv93">discord channel for testing here</a>.</p></body></html> + + + true + + + + + + + Upgrade the instance + + + + + + + + diff --git a/ultimmc/launcher/ui/pages/instance/LogPage.cpp b/ultimmc/launcher/ui/pages/instance/LogPage.cpp new file mode 100644 index 0000000..bd25c20 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/LogPage.cpp @@ -0,0 +1,343 @@ +#include "LogPage.h" +#include "ui_LogPage.h" + +#include "Application.h" + +#include +#include +#include + +#include "launch/LaunchTask.h" +#include "settings/Setting.h" + +#include "ui/GuiUtil.h" +#include "ui/ColorCache.h" +#include "ui/dialogs/CustomMessageBox.h" + +#include + +class LogFormatProxyModel : public QIdentityProxyModel +{ +public: + LogFormatProxyModel(QObject* parent = nullptr) : QIdentityProxyModel(parent) + { + } + QVariant data(const QModelIndex &index, int role) const override + { + switch(role) + { + case Qt::FontRole: + return m_font; + case Qt::TextColorRole: + { + MessageLevel::Enum level = (MessageLevel::Enum) QIdentityProxyModel::data(index, LogModel::LevelRole).toInt(); + return m_colors->getFront(level); + } + case Qt::BackgroundRole: + { + MessageLevel::Enum level = (MessageLevel::Enum) QIdentityProxyModel::data(index, LogModel::LevelRole).toInt(); + return m_colors->getBack(level); + } + default: + return QIdentityProxyModel::data(index, role); + } + } + + void setFont(QFont font) + { + m_font = font; + } + + void setColors(LogColorCache* colors) + { + m_colors.reset(colors); + } + + QModelIndex find(const QModelIndex &start, const QString &value, bool reverse) const + { + QModelIndex parentIndex = parent(start); + auto compare = [&](int r) -> QModelIndex + { + QModelIndex idx = index(r, start.column(), parentIndex); + if (!idx.isValid() || idx == start) + { + return QModelIndex(); + } + QVariant v = data(idx, Qt::DisplayRole); + QString t = v.toString(); + if (t.contains(value, Qt::CaseInsensitive)) + return idx; + return QModelIndex(); + }; + if(reverse) + { + int from = start.row(); + int to = 0; + + for (int i = 0; i < 2; ++i) + { + for (int r = from; (r >= to); --r) + { + auto idx = compare(r); + if(idx.isValid()) + return idx; + } + // prepare for the next iteration + from = rowCount() - 1; + to = start.row(); + } + } + else + { + int from = start.row(); + int to = rowCount(parentIndex); + + for (int i = 0; i < 2; ++i) + { + for (int r = from; (r < to); ++r) + { + auto idx = compare(r); + if(idx.isValid()) + return idx; + } + // prepare for the next iteration + from = 0; + to = start.row(); + } + } + return QModelIndex(); + } +private: + QFont m_font; + std::unique_ptr m_colors; +}; + +LogPage::LogPage(InstancePtr instance, QWidget *parent) + : QWidget(parent), ui(new Ui::LogPage), m_instance(instance) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + + m_proxy = new LogFormatProxyModel(this); + // set up text colors in the log proxy and adapt them to the current theme foreground and background + { + auto origForeground = ui->text->palette().color(ui->text->foregroundRole()); + auto origBackground = ui->text->palette().color(ui->text->backgroundRole()); + m_proxy->setColors(new LogColorCache(origForeground, origBackground)); + } + + // set up fonts in the log proxy + { + QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); + bool conversionOk = false; + int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); + if(!conversionOk) + { + fontSize = 11; + } + m_proxy->setFont(QFont(fontFamily, fontSize)); + } + + ui->text->setModel(m_proxy); + + // set up instance and launch process recognition + { + auto launchTask = m_instance->getLaunchTask(); + if(launchTask) + { + setInstanceLaunchTaskChanged(launchTask, true); + } + connect(m_instance.get(), &BaseInstance::launchTaskChanged, this, &LogPage::onInstanceLaunchTaskChanged); + } + + auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); + connect(findShortcut, SIGNAL(activated()), SLOT(findActivated())); + auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this); + connect(findNextShortcut, SIGNAL(activated()), SLOT(findNextActivated())); + connect(ui->searchBar, SIGNAL(returnPressed()), SLOT(on_findButton_clicked())); + auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); + connect(findPreviousShortcut, SIGNAL(activated()), SLOT(findPreviousActivated())); +} + +LogPage::~LogPage() +{ + delete ui; +} + +void LogPage::modelStateToUI() +{ + if(m_model->wrapLines()) + { + ui->text->setWordWrap(true); + ui->wrapCheckbox->setCheckState(Qt::Checked); + } + else + { + ui->text->setWordWrap(false); + ui->wrapCheckbox->setCheckState(Qt::Unchecked); + } + if(m_model->suspended()) + { + ui->trackLogCheckbox->setCheckState(Qt::Unchecked); + } + else + { + ui->trackLogCheckbox->setCheckState(Qt::Checked); + } +} + +void LogPage::UIToModelState() +{ + if(!m_model) + { + return; + } + m_model->setLineWrap(ui->wrapCheckbox->checkState() == Qt::Checked); + m_model->suspend(ui->trackLogCheckbox->checkState() != Qt::Checked); +} + +void LogPage::setInstanceLaunchTaskChanged(shared_qobject_ptr proc, bool initial) +{ + m_process = proc; + if(m_process) + { + m_model = proc->getLogModel(); + m_proxy->setSourceModel(m_model.get()); + if(initial) + { + modelStateToUI(); + } + else + { + UIToModelState(); + } + } + else + { + m_proxy->setSourceModel(nullptr); + m_model.reset(); + } +} + +void LogPage::onInstanceLaunchTaskChanged(shared_qobject_ptr proc) +{ + setInstanceLaunchTaskChanged(proc, false); +} + +bool LogPage::apply() +{ + return true; +} + +bool LogPage::shouldDisplay() const +{ + return m_instance->isRunning() || m_proxy->rowCount() > 0; +} + +void LogPage::on_btnPaste_clicked() +{ + if(!m_model) + return; + + auto response = CustomMessageBox::selectable( + this, + tr("Log upload"), + tr("Are you sure you want to upload this log file?"), + QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No + )->exec(); + + if (response != QMessageBox::Yes) + return; + + //FIXME: turn this into a proper task and move the upload logic out of GuiUtil! + m_model->append( + MessageLevel::Launcher, + QString("%2: Log upload triggered at: %1").arg( + QDateTime::currentDateTime().toString(Qt::RFC2822Date), + BuildConfig.LAUNCHER_NAME + ) + ); + auto url = GuiUtil::uploadPaste(m_model->toPlainText(), this); + if(!url.isEmpty()) + { + m_model->append( + MessageLevel::Launcher, + QString("%2: Log uploaded to: %1").arg( + url, + BuildConfig.LAUNCHER_NAME + ) + ); + } + else + { + m_model->append( + MessageLevel::Error, + QString("%1: Log upload failed!").arg(BuildConfig.LAUNCHER_NAME) + ); + } +} + +void LogPage::on_btnCopy_clicked() +{ + if(!m_model) + return; + m_model->append(MessageLevel::Launcher, QString("Clipboard copy at: %1").arg(QDateTime::currentDateTime().toString(Qt::RFC2822Date))); + GuiUtil::setClipboardText(m_model->toPlainText()); +} + +void LogPage::on_btnClear_clicked() +{ + if(!m_model) + return; + m_model->clear(); + m_container->refreshContainer(); +} + +void LogPage::on_btnBottom_clicked() +{ + ui->text->scrollToBottom(); +} + +void LogPage::on_trackLogCheckbox_clicked(bool checked) +{ + if(!m_model) + return; + m_model->suspend(!checked); +} + +void LogPage::on_wrapCheckbox_clicked(bool checked) +{ + ui->text->setWordWrap(checked); + if(!m_model) + return; + m_model->setLineWrap(checked); +} + +void LogPage::on_findButton_clicked() +{ + auto modifiers = QApplication::keyboardModifiers(); + bool reverse = modifiers & Qt::ShiftModifier; + ui->text->findNext(ui->searchBar->text(), reverse); +} + +void LogPage::findNextActivated() +{ + ui->text->findNext(ui->searchBar->text(), false); +} + +void LogPage::findPreviousActivated() +{ + ui->text->findNext(ui->searchBar->text(), true); +} + +void LogPage::findActivated() +{ + // focus the search bar if it doesn't have focus + if (!ui->searchBar->hasFocus()) + { + ui->searchBar->setFocus(); + ui->searchBar->selectAll(); + } +} diff --git a/ultimmc/launcher/ui/pages/instance/LogPage.h b/ultimmc/launcher/ui/pages/instance/LogPage.h new file mode 100644 index 0000000..cab2556 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/LogPage.h @@ -0,0 +1,86 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "BaseInstance.h" +#include "launch/LaunchTask.h" +#include "ui/pages/BasePage.h" +#include + +namespace Ui +{ +class LogPage; +} +class QTextCharFormat; +class LogFormatProxyModel; + +class LogPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit LogPage(InstancePtr instance, QWidget *parent = 0); + virtual ~LogPage(); + virtual QString displayName() const override + { + return tr("Minecraft Log"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("log"); + } + virtual QString id() const override + { + return "console"; + } + virtual bool apply() override; + virtual QString helpPage() const override + { + return "Minecraft-Logs"; + } + virtual bool shouldDisplay() const override; + +private slots: + void on_btnPaste_clicked(); + void on_btnCopy_clicked(); + void on_btnClear_clicked(); + void on_btnBottom_clicked(); + + void on_trackLogCheckbox_clicked(bool checked); + void on_wrapCheckbox_clicked(bool checked); + + void on_findButton_clicked(); + void findActivated(); + void findNextActivated(); + void findPreviousActivated(); + + void onInstanceLaunchTaskChanged(shared_qobject_ptr proc); + +private: + void modelStateToUI(); + void UIToModelState(); + void setInstanceLaunchTaskChanged(shared_qobject_ptr proc, bool initial); + +private: + Ui::LogPage *ui; + InstancePtr m_instance; + shared_qobject_ptr m_process; + + LogFormatProxyModel * m_proxy; + shared_qobject_ptr m_model; +}; diff --git a/ultimmc/launcher/ui/pages/instance/LogPage.ui b/ultimmc/launcher/ui/pages/instance/LogPage.ui new file mode 100644 index 0000000..ccfc155 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/LogPage.ui @@ -0,0 +1,182 @@ + + + LogPage + + + + 0 + 0 + 825 + 782 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + Tab 1 + + + + + + false + + + true + + + + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + false + + + + + + + + + Keep updating + + + true + + + + + + + Wrap lines + + + true + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Copy the whole log into the clipboard + + + &Copy + + + + + + + Upload the log to paste.ee - it will stay online for a month + + + Upload + + + + + + + Clear the log + + + Clear + + + + + + + + + Search: + + + + + + + Find + + + + + + + + + + Scroll all the way to bottom + + + Bottom + + + + + + + Qt::Vertical + + + + + + + + + + + + LogView + QPlainTextEdit +
ui/widgets/LogView.h
+
+
+ + tabWidget + trackLogCheckbox + wrapCheckbox + btnCopy + btnPaste + btnClear + text + searchBar + findButton + + + +
diff --git a/ultimmc/launcher/ui/pages/instance/ModFolderPage.cpp b/ultimmc/launcher/ui/pages/instance/ModFolderPage.cpp new file mode 100644 index 0000000..e63b143 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/ModFolderPage.cpp @@ -0,0 +1,366 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModFolderPage.h" +#include "ui_ModFolderPage.h" + +#include +#include +#include +#include +#include +#include + +#include "Application.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/GuiUtil.h" + +#include "DesktopServices.h" + +#include "minecraft/mod/ModFolderModel.h" +#include "minecraft/mod/Mod.h" +#include "minecraft/VersionFilterData.h" +#include "minecraft/PackProfile.h" + +#include "Version.h" + +namespace { + // FIXME: wasteful + void RemoveThePrefix(QString & string) { + QRegularExpression regex(QStringLiteral("^(([Tt][Hh][eE])|([Tt][eE][Hh])) +")); + string.remove(regex); + string = string.trimmed(); + } +} + +class ModSortProxy : public QSortFilterProxyModel +{ +public: + explicit ModSortProxy(QObject *parent = 0) : QSortFilterProxyModel(parent) + { + } + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex & source_parent) const override { + ModFolderModel *model = qobject_cast(sourceModel()); + if(!model) { + return false; + } + const auto &mod = model->at(source_row); + if(mod.name().contains(filterRegExp())) { + return true; + } + if(mod.description().contains(filterRegExp())) { + return true; + } + for(auto & author: mod.authors()) { + if (author.contains(filterRegExp())) { + return true; + } + } + return false; + } + + bool lessThan(const QModelIndex & source_left, const QModelIndex & source_right) const override + { + ModFolderModel *model = qobject_cast(sourceModel()); + if( + !model || + !source_left.isValid() || + !source_right.isValid() || + source_left.column() != source_right.column() + ) { + return QSortFilterProxyModel::lessThan(source_left, source_right); + } + + // we are now guaranteed to have two valid indexes in the same column... we love the provided invariants unconditionally and proceed. + + auto column = (ModFolderModel::Columns) source_left.column(); + bool invert = false; + switch(column) { + // GH-2550 - sort by enabled/disabled + case ModFolderModel::ActiveColumn: { + auto dataL = source_left.data(Qt::CheckStateRole).toBool(); + auto dataR = source_right.data(Qt::CheckStateRole).toBool(); + if(dataL != dataR) { + return dataL > dataR; + } + // fallthrough + invert = sortOrder() == Qt::DescendingOrder; + } + // GH-2722 - sort mod names in a way that discards "The" prefixes + case ModFolderModel::NameColumn: { + auto dataL = model->data(model->index(source_left.row(), ModFolderModel::NameColumn)).toString(); + RemoveThePrefix(dataL); + auto dataR = model->data(model->index(source_right.row(), ModFolderModel::NameColumn)).toString(); + RemoveThePrefix(dataR); + + auto less = dataL.compare(dataR, sortCaseSensitivity()); + if(less != 0) { + return invert ? (less > 0) : (less < 0); + } + // fallthrough + invert = sortOrder() == Qt::DescendingOrder; + } + // GH-2762 - sort versions by parsing them as versions + case ModFolderModel::VersionColumn: { + auto dataL = Version(model->data(model->index(source_left.row(), ModFolderModel::VersionColumn)).toString()); + auto dataR = Version(model->data(model->index(source_right.row(), ModFolderModel::VersionColumn)).toString()); + return invert ? (dataL > dataR) : (dataL < dataR); + } + default: { + return QSortFilterProxyModel::lessThan(source_left, source_right); + } + } + } +}; + +ModFolderPage::ModFolderPage( + BaseInstance *inst, + std::shared_ptr mods, + QString id, + QString iconName, + QString displayName, + QString helpPage, + QWidget *parent +) : + QMainWindow(parent), + ui(new Ui::ModFolderPage) +{ + ui->setupUi(this); + ui->actionsToolbar->insertSpacer(ui->actionView_configs); + + m_inst = inst; + on_RunningState_changed(m_inst && m_inst->isRunning()); + m_mods = mods; + m_id = id; + m_displayName = displayName; + m_iconName = iconName; + m_helpName = helpPage; + m_fileSelectionFilter = "%1 (*.zip *.jar)"; + m_filterModel = new ModSortProxy(this); + m_filterModel->setDynamicSortFilter(true); + m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); + m_filterModel->setSourceModel(m_mods.get()); + m_filterModel->setFilterKeyColumn(-1); + ui->modTreeView->setModel(m_filterModel); + ui->modTreeView->installEventFilter(this); + ui->modTreeView->sortByColumn(1, Qt::AscendingOrder); + ui->modTreeView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->modTreeView, &ModListView::customContextMenuRequested, this, &ModFolderPage::ShowContextMenu); + connect(ui->modTreeView, &ModListView::activated, this, &ModFolderPage::modItemActivated); + + auto smodel = ui->modTreeView->selectionModel(); + connect(smodel, &QItemSelectionModel::currentChanged, this, &ModFolderPage::modCurrent); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &ModFolderPage::on_filterTextChanged); + connect(m_inst, &BaseInstance::runningStatusChanged, this, &ModFolderPage::on_RunningState_changed); +} + +void ModFolderPage::modItemActivated(const QModelIndex&) +{ + if(!m_controlsEnabled) { + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection()); + m_mods->setModStatus(selection.indexes(), ModFolderModel::Toggle); +} + +QMenu * ModFolderPage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction(ui->actionsToolbar->toggleViewAction() ); + return filteredMenu; +} + +void ModFolderPage::ShowContextMenu(const QPoint& pos) +{ + auto menu = ui->actionsToolbar->createContextMenu(this, tr("Context menu")); + menu->exec(ui->modTreeView->mapToGlobal(pos)); + delete menu; +} + +void ModFolderPage::openedImpl() +{ + m_mods->startWatching(); +} + +void ModFolderPage::closedImpl() +{ + m_mods->stopWatching(); +} + +void ModFolderPage::on_filterTextChanged(const QString& newContents) +{ + m_viewFilter = newContents; + m_filterModel->setFilterFixedString(m_viewFilter); +} + + +CoreModFolderPage::CoreModFolderPage(BaseInstance *inst, std::shared_ptr mods, + QString id, QString iconName, QString displayName, + QString helpPage, QWidget *parent) + : ModFolderPage(inst, mods, id, iconName, displayName, helpPage, parent) +{ +} + +ModFolderPage::~ModFolderPage() +{ + m_mods->stopWatching(); + delete ui; +} + +void ModFolderPage::on_RunningState_changed(bool running) +{ + if(m_controlsEnabled == !running) { + return; + } + m_controlsEnabled = !running; + ui->actionAdd->setEnabled(m_controlsEnabled); + ui->actionDisable->setEnabled(m_controlsEnabled); + ui->actionEnable->setEnabled(m_controlsEnabled); + ui->actionRemove->setEnabled(m_controlsEnabled); +} + +bool ModFolderPage::shouldDisplay() const +{ + return true; +} + +bool CoreModFolderPage::shouldDisplay() const +{ + if (ModFolderPage::shouldDisplay()) + { + auto inst = dynamic_cast(m_inst); + if (!inst) + return true; + auto version = inst->getPackProfile(); + if (!version) + return true; + if(!version->getComponent("net.minecraftforge")) + { + return false; + } + if(!version->getComponent("net.minecraft")) + { + return false; + } + if(version->getComponent("net.minecraft")->getReleaseDateTime() < g_VersionFilterData.legacyCutoffDate) + { + return true; + } + } + return false; +} + +bool ModFolderPage::modListFilter(QKeyEvent *keyEvent) +{ + switch (keyEvent->key()) + { + case Qt::Key_Delete: + on_actionRemove_triggered(); + return true; + case Qt::Key_Plus: + on_actionAdd_triggered(); + return true; + default: + break; + } + return QWidget::eventFilter(ui->modTreeView, keyEvent); +} + +bool ModFolderPage::eventFilter(QObject *obj, QEvent *ev) +{ + if (ev->type() != QEvent::KeyPress) + { + return QWidget::eventFilter(obj, ev); + } + QKeyEvent *keyEvent = static_cast(ev); + if (obj == ui->modTreeView) + return modListFilter(keyEvent); + return QWidget::eventFilter(obj, ev); +} + +void ModFolderPage::on_actionAdd_triggered() +{ + if(!m_controlsEnabled) { + return; + } + auto list = GuiUtil::BrowseForFiles( + m_helpName, + tr("Select %1", + "Select whatever type of files the page contains. Example: 'Loader Mods'") + .arg(m_displayName), + m_fileSelectionFilter.arg(m_displayName), APPLICATION->settings()->get("CentralModsDir").toString(), + this->parentWidget()); + if (!list.empty()) + { + for (auto filename : list) + { + m_mods->installMod(filename); + } + } +} + +void ModFolderPage::on_actionEnable_triggered() +{ + if(!m_controlsEnabled) { + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection()); + m_mods->setModStatus(selection.indexes(), ModFolderModel::Enable); +} + +void ModFolderPage::on_actionDisable_triggered() +{ + if(!m_controlsEnabled) { + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection()); + m_mods->setModStatus(selection.indexes(), ModFolderModel::Disable); +} + +void ModFolderPage::on_actionRemove_triggered() +{ + if(!m_controlsEnabled) { + return; + } + auto selection = m_filterModel->mapSelectionToSource(ui->modTreeView->selectionModel()->selection()); + m_mods->deleteMods(selection.indexes()); +} + +void ModFolderPage::on_actionView_configs_triggered() +{ + DesktopServices::openDirectory(m_inst->instanceConfigFolder(), true); +} + +void ModFolderPage::on_actionView_Folder_triggered() +{ + DesktopServices::openDirectory(m_mods->dir().absolutePath(), true); +} + +void ModFolderPage::modCurrent(const QModelIndex ¤t, const QModelIndex &previous) +{ + if (!current.isValid()) + { + ui->frame->clear(); + return; + } + auto sourceCurrent = m_filterModel->mapToSource(current); + int row = sourceCurrent.row(); + Mod &m = m_mods->operator[](row); + ui->frame->updateWithMod(m); +} diff --git a/ultimmc/launcher/ui/pages/instance/ModFolderPage.h b/ultimmc/launcher/ui/pages/instance/ModFolderPage.h new file mode 100644 index 0000000..8ef7559 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/ModFolderPage.h @@ -0,0 +1,120 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "minecraft/MinecraftInstance.h" +#include "ui/pages/BasePage.h" + +#include + +class ModFolderModel; +namespace Ui +{ +class ModFolderPage; +} + +class ModFolderPage : public QMainWindow, public BasePage +{ + Q_OBJECT + +public: + explicit ModFolderPage( + BaseInstance *inst, + std::shared_ptr mods, + QString id, + QString iconName, + QString displayName, + QString helpPage = "", + QWidget *parent = 0 + ); + virtual ~ModFolderPage(); + + void setFilter(const QString & filter) + { + m_fileSelectionFilter = filter; + } + + virtual QString displayName() const override + { + return m_displayName; + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon(m_iconName); + } + virtual QString id() const override + { + return m_id; + } + virtual QString helpPage() const override + { + return m_helpName; + } + virtual bool shouldDisplay() const override; + + virtual void openedImpl() override; + virtual void closedImpl() override; +protected: + bool eventFilter(QObject *obj, QEvent *ev) override; + bool modListFilter(QKeyEvent *ev); + QMenu * createPopupMenu() override; + +protected: + BaseInstance *m_inst = nullptr; + +protected: + Ui::ModFolderPage *ui = nullptr; + std::shared_ptr m_mods; + QSortFilterProxyModel *m_filterModel = nullptr; + QString m_iconName; + QString m_id; + QString m_displayName; + QString m_helpName; + QString m_fileSelectionFilter; + QString m_viewFilter; + bool m_controlsEnabled = true; + +public +slots: + void modCurrent(const QModelIndex ¤t, const QModelIndex &previous); + +private +slots: + void modItemActivated(const QModelIndex &index); + void on_filterTextChanged(const QString & newContents); + void on_RunningState_changed(bool running); + void on_actionAdd_triggered(); + void on_actionRemove_triggered(); + void on_actionEnable_triggered(); + void on_actionDisable_triggered(); + void on_actionView_Folder_triggered(); + void on_actionView_configs_triggered(); + void ShowContextMenu(const QPoint &pos); +}; + +class CoreModFolderPage : public ModFolderPage +{ +public: + explicit CoreModFolderPage(BaseInstance *inst, std::shared_ptr mods, QString id, + QString iconName, QString displayName, QString helpPage = "", + QWidget *parent = 0); + virtual ~CoreModFolderPage() + { + } + virtual bool shouldDisplay() const; +}; diff --git a/ultimmc/launcher/ui/pages/instance/ModFolderPage.ui b/ultimmc/launcher/ui/pages/instance/ModFolderPage.ui new file mode 100644 index 0000000..0fb51e8 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/ModFolderPage.ui @@ -0,0 +1,164 @@ + + + ModFolderPage + + + + 0 + 0 + 1042 + 501 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + true + + + + + + + Filter: + + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + true + + + QAbstractItemView::DropOnly + + + + + + + + Actions + + + Qt::ToolButtonTextOnly + + + RightToolBarArea + + + false + + + + + + + + + + + + &Add + + + Add mods + + + + + &Remove + + + Remove selected mods + + + + + &Enable + + + Enable selected mods + + + + + &Disable + + + Disable selected mods + + + + + View &Configs + + + Open the 'config' folder in the system file manager. + + + + + View &Folder + + + + + + ModListView + QTreeView +
ui/widgets/ModListView.h
+
+ + MCModInfoFrame + QFrame +
ui/widgets/MCModInfoFrame.h
+ 1 +
+ + WideBar + QToolBar +
ui/widgets/WideBar.h
+
+
+ + modTreeView + filterEdit + + + +
diff --git a/ultimmc/launcher/ui/pages/instance/NotesPage.cpp b/ultimmc/launcher/ui/pages/instance/NotesPage.cpp new file mode 100644 index 0000000..fa966c9 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/NotesPage.cpp @@ -0,0 +1,21 @@ +#include "NotesPage.h" +#include "ui_NotesPage.h" +#include + +NotesPage::NotesPage(BaseInstance *inst, QWidget *parent) + : QWidget(parent), ui(new Ui::NotesPage), m_inst(inst) +{ + ui->setupUi(this); + ui->noteEditor->setText(m_inst->notes()); +} + +NotesPage::~NotesPage() +{ + delete ui; +} + +bool NotesPage::apply() +{ + m_inst->setNotes(ui->noteEditor->toPlainText()); + return true; +} diff --git a/ultimmc/launcher/ui/pages/instance/NotesPage.h b/ultimmc/launcher/ui/pages/instance/NotesPage.h new file mode 100644 index 0000000..539401e --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/NotesPage.h @@ -0,0 +1,60 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "BaseInstance.h" +#include "ui/pages/BasePage.h" +#include + +namespace Ui +{ +class NotesPage; +} + +class NotesPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit NotesPage(BaseInstance *inst, QWidget *parent = 0); + virtual ~NotesPage(); + virtual QString displayName() const override + { + return tr("Notes"); + } + virtual QIcon icon() const override + { + auto icon = APPLICATION->getThemedIcon("notes"); + if(icon.isNull()) + icon = APPLICATION->getThemedIcon("news"); + return icon; + } + virtual QString id() const override + { + return "notes"; + } + virtual bool apply() override; + virtual QString helpPage() const override + { + return "Notes"; + } + +private: + Ui::NotesPage *ui; + BaseInstance *m_inst; +}; diff --git a/ultimmc/launcher/ui/pages/instance/NotesPage.ui b/ultimmc/launcher/ui/pages/instance/NotesPage.ui new file mode 100644 index 0000000..67cb261 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/NotesPage.ui @@ -0,0 +1,49 @@ + + + NotesPage + + + + 0 + 0 + 731 + 538 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::ScrollBarAlwaysOn + + + true + + + false + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextEditable|Qt::TextEditorInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + noteEditor + + + + diff --git a/ultimmc/launcher/ui/pages/instance/OtherLogsPage.cpp b/ultimmc/launcher/ui/pages/instance/OtherLogsPage.cpp new file mode 100644 index 0000000..3a085f0 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/OtherLogsPage.cpp @@ -0,0 +1,327 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "OtherLogsPage.h" +#include "ui_OtherLogsPage.h" + +#include + +#include "ui/GuiUtil.h" + +#include "RecursiveFileSystemWatcher.h" +#include "ui/dialogs/CustomMessageBox.h" +#include +#include +#include + +OtherLogsPage::OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget *parent) + : QWidget(parent), ui(new Ui::OtherLogsPage), m_path(path), m_fileFilter(fileFilter), + m_watcher(new RecursiveFileSystemWatcher(this)) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + + m_watcher->setMatcher(fileFilter); + m_watcher->setRootDir(QDir::current().absoluteFilePath(m_path)); + + connect(m_watcher, &RecursiveFileSystemWatcher::filesChanged, this, &OtherLogsPage::populateSelectLogBox); + populateSelectLogBox(); + + auto findShortcut = new QShortcut(QKeySequence(QKeySequence::Find), this); + connect(findShortcut, &QShortcut::activated, this, &OtherLogsPage::findActivated); + + auto findNextShortcut = new QShortcut(QKeySequence(QKeySequence::FindNext), this); + connect(findNextShortcut, &QShortcut::activated, this, &OtherLogsPage::findNextActivated); + + auto findPreviousShortcut = new QShortcut(QKeySequence(QKeySequence::FindPrevious), this); + connect(findPreviousShortcut, &QShortcut::activated, this, &OtherLogsPage::findPreviousActivated); + + connect(ui->searchBar, &QLineEdit::returnPressed, this, &OtherLogsPage::on_findButton_clicked); +} + +OtherLogsPage::~OtherLogsPage() +{ + delete ui; +} + +void OtherLogsPage::openedImpl() +{ + m_watcher->enable(); +} +void OtherLogsPage::closedImpl() +{ + m_watcher->disable(); +} + +void OtherLogsPage::populateSelectLogBox() +{ + ui->selectLogBox->clear(); + ui->selectLogBox->addItems(m_watcher->files()); + if (m_currentFile.isEmpty()) + { + setControlsEnabled(false); + ui->selectLogBox->setCurrentIndex(-1); + } + else + { + const int index = ui->selectLogBox->findText(m_currentFile); + if (index != -1) + { + ui->selectLogBox->setCurrentIndex(index); + setControlsEnabled(true); + } + else + { + setControlsEnabled(false); + } + } +} + +void OtherLogsPage::on_selectLogBox_currentIndexChanged(const int index) +{ + QString file; + if (index != -1) + { + file = ui->selectLogBox->itemText(index); + } + + if (file.isEmpty() || !QFile::exists(FS::PathCombine(m_path, file))) + { + m_currentFile = QString(); + ui->text->clear(); + setControlsEnabled(false); + } + else + { + m_currentFile = file; + on_btnReload_clicked(); + setControlsEnabled(true); + } +} + +void OtherLogsPage::on_btnReload_clicked() +{ + if(m_currentFile.isEmpty()) + { + setControlsEnabled(false); + return; + } + QFile file(FS::PathCombine(m_path, m_currentFile)); + if (!file.open(QFile::ReadOnly)) + { + setControlsEnabled(false); + ui->btnReload->setEnabled(true); // allow reload + m_currentFile = QString(); + QMessageBox::critical(this, tr("Error"), tr("Unable to open %1 for reading: %2") + .arg(m_currentFile, file.errorString())); + } + else + { + auto setPlainText = [&](const QString & text) + { + QString fontFamily = APPLICATION->settings()->get("ConsoleFont").toString(); + bool conversionOk = false; + int fontSize = APPLICATION->settings()->get("ConsoleFontSize").toInt(&conversionOk); + if(!conversionOk) + { + fontSize = 11; + } + QTextDocument *doc = ui->text->document(); + doc->setDefaultFont(QFont(fontFamily, fontSize)); + ui->text->setPlainText(text); + }; + auto showTooBig = [&]() + { + setPlainText( + tr("The file (%1) is too big. You may want to open it in a viewer optimized " + "for large files.").arg(file.fileName())); + }; + if(file.size() > (1024ll * 1024ll * 12ll)) + { + showTooBig(); + return; + } + QString content; + if(file.fileName().endsWith(".gz")) + { + QByteArray temp; + if(!GZip::unzip(file.readAll(), temp)) + { + setPlainText( + tr("The file (%1) is not readable.").arg(file.fileName())); + return; + } + content = QString::fromUtf8(temp); + } + else + { + content = QString::fromUtf8(file.readAll()); + } + if (content.size() >= 50000000ll) + { + showTooBig(); + return; + } + setPlainText(content); + } +} + +void OtherLogsPage::on_btnPaste_clicked() +{ + auto response = CustomMessageBox::selectable( + this, + tr("Log upload"), + tr("Are you sure you want to upload this log file?"), + QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::No + )->exec(); + + if (response != QMessageBox::Yes) + return; + + GuiUtil::uploadPaste(ui->text->toPlainText(), this); +} + +void OtherLogsPage::on_btnCopy_clicked() +{ + GuiUtil::setClipboardText(ui->text->toPlainText()); +} + +void OtherLogsPage::on_btnDelete_clicked() +{ + if(m_currentFile.isEmpty()) + { + setControlsEnabled(false); + return; + } + if (QMessageBox::question(this, tr("Delete"), + tr("Do you really want to delete %1?").arg(m_currentFile), + QMessageBox::Yes, QMessageBox::No) == QMessageBox::No) + { + return; + } + QFile file(FS::PathCombine(m_path, m_currentFile)); + if (!file.remove()) + { + QMessageBox::critical(this, tr("Error"), tr("Unable to delete %1: %2") + .arg(m_currentFile, file.errorString())); + } +} + + + +void OtherLogsPage::on_btnClean_clicked() +{ + auto toDelete = m_watcher->files(); + if(toDelete.isEmpty()) + { + return; + } + QMessageBox *messageBox = new QMessageBox(this); + messageBox->setWindowTitle(tr("Clean up")); + if(toDelete.size() > 5) + { + messageBox->setText(tr("Do you really want to delete all log files?")); + messageBox->setDetailedText(toDelete.join('\n')); + } + else + { + messageBox->setText(tr("Do you really want to delete these files?\n%1").arg(toDelete.join('\n'))); + } + messageBox->setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + messageBox->setDefaultButton(QMessageBox::Ok); + messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBox->setIcon(QMessageBox::Question); + messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + + if (messageBox->exec() != QMessageBox::Ok) + { + return; + } + QStringList failed; + for(auto item: toDelete) + { + QFile file(FS::PathCombine(m_path, item)); + if (!file.remove()) + { + failed.push_back(item); + } + } + if(!failed.empty()) + { + QMessageBox *messageBox = new QMessageBox(this); + messageBox->setWindowTitle(tr("Error")); + if(failed.size() > 5) + { + messageBox->setText(tr("Couldn't delete some files!")); + messageBox->setDetailedText(failed.join('\n')); + } + else + { + messageBox->setText(tr("Couldn't delete some files:\n%1").arg(failed.join('\n'))); + } + messageBox->setStandardButtons(QMessageBox::Ok); + messageBox->setDefaultButton(QMessageBox::Ok); + messageBox->setTextInteractionFlags(Qt::TextSelectableByMouse); + messageBox->setIcon(QMessageBox::Critical); + messageBox->setTextInteractionFlags(Qt::TextBrowserInteraction); + messageBox->exec(); + } +} + + +void OtherLogsPage::setControlsEnabled(const bool enabled) +{ + ui->btnReload->setEnabled(enabled); + ui->btnDelete->setEnabled(enabled); + ui->btnCopy->setEnabled(enabled); + ui->btnPaste->setEnabled(enabled); + ui->text->setEnabled(enabled); + ui->btnClean->setEnabled(enabled); +} + +// FIXME: HACK, use LogView instead? +static void findNext(QPlainTextEdit * _this, const QString& what, bool reverse) +{ + _this->find(what, reverse ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0)); +} + +void OtherLogsPage::on_findButton_clicked() +{ + auto modifiers = QApplication::keyboardModifiers(); + bool reverse = modifiers & Qt::ShiftModifier; + findNext(ui->text, ui->searchBar->text(), reverse); +} + +void OtherLogsPage::findNextActivated() +{ + findNext(ui->text, ui->searchBar->text(), false); +} + +void OtherLogsPage::findPreviousActivated() +{ + findNext(ui->text, ui->searchBar->text(), true); +} + +void OtherLogsPage::findActivated() +{ + // focus the search bar if it doesn't have focus + if (!ui->searchBar->hasFocus()) + { + ui->searchBar->setFocus(); + ui->searchBar->selectAll(); + } +} diff --git a/ultimmc/launcher/ui/pages/instance/OtherLogsPage.h b/ultimmc/launcher/ui/pages/instance/OtherLogsPage.h new file mode 100644 index 0000000..b2b2a91 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/OtherLogsPage.h @@ -0,0 +1,81 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "ui/pages/BasePage.h" +#include +#include + +namespace Ui +{ +class OtherLogsPage; +} + +class RecursiveFileSystemWatcher; + +class OtherLogsPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit OtherLogsPage(QString path, IPathMatcher::Ptr fileFilter, QWidget *parent = 0); + ~OtherLogsPage(); + + QString id() const override + { + return "logs"; + } + QString displayName() const override + { + return tr("Other logs"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("log"); + } + QString helpPage() const override + { + return "Minecraft-Logs"; + } + void openedImpl() override; + void closedImpl() override; + +private slots: + void populateSelectLogBox(); + void on_selectLogBox_currentIndexChanged(const int index); + void on_btnReload_clicked(); + void on_btnPaste_clicked(); + void on_btnCopy_clicked(); + void on_btnDelete_clicked(); + void on_btnClean_clicked(); + + void on_findButton_clicked(); + void findActivated(); + void findNextActivated(); + void findPreviousActivated(); + +private: + void setControlsEnabled(const bool enabled); + +private: + Ui::OtherLogsPage *ui; + QString m_path; + QString m_currentFile; + IPathMatcher::Ptr m_fileFilter; + RecursiveFileSystemWatcher *m_watcher; +}; diff --git a/ultimmc/launcher/ui/pages/instance/OtherLogsPage.ui b/ultimmc/launcher/ui/pages/instance/OtherLogsPage.ui new file mode 100644 index 0000000..56ff3b6 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/OtherLogsPage.ui @@ -0,0 +1,150 @@ + + + OtherLogsPage + + + + 0 + 0 + 657 + 538 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + Tab 1 + + + + + + + + + Find + + + + + + + false + + + Qt::ScrollBarAlwaysOn + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + Copy the whole log into the clipboard + + + &Copy + + + + + + + Clear the log + + + Delete + + + + + + + Upload the log to paste.ee - it will stay online for a month + + + Upload + + + + + + + Clear the log + + + Clean + + + + + + + Reload + + + + + + + + 0 + 0 + + + + + + + + + + Search: + + + + + + + + + + + tabWidget + selectLogBox + btnReload + btnCopy + btnPaste + btnDelete + btnClean + text + searchBar + findButton + + + + diff --git a/ultimmc/launcher/ui/pages/instance/ResourcePackPage.h b/ultimmc/launcher/ui/pages/instance/ResourcePackPage.h new file mode 100644 index 0000000..1486bf5 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/ResourcePackPage.h @@ -0,0 +1,23 @@ +#pragma once + +#include "ModFolderPage.h" +#include "ui_ModFolderPage.h" + +class ResourcePackPage : public ModFolderPage +{ + Q_OBJECT +public: + explicit ResourcePackPage(MinecraftInstance *instance, QWidget *parent = 0) + : ModFolderPage(instance, instance->resourcePackList(), "resourcepacks", + "resourcepacks", tr("Resource packs"), "Resource-packs", parent) + { + ui->actionView_configs->setVisible(false); + } + virtual ~ResourcePackPage() {} + + virtual bool shouldDisplay() const override + { + return !m_inst->traits().contains("no-texturepacks") && + !m_inst->traits().contains("texturepacks"); + } +}; diff --git a/ultimmc/launcher/ui/pages/instance/ScreenshotsPage.cpp b/ultimmc/launcher/ui/pages/instance/ScreenshotsPage.cpp new file mode 100644 index 0000000..e70117e --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/ScreenshotsPage.cpp @@ -0,0 +1,489 @@ +#include "ScreenshotsPage.h" +#include "ui_ScreenshotsPage.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "ui/dialogs/ProgressDialog.h" +#include "ui/dialogs/CustomMessageBox.h" + +#include "net/NetJob.h" +#include "screenshots/ImgurUpload.h" +#include "screenshots/ImgurAlbumCreation.h" +#include "tasks/SequentialTask.h" + +#include "RWStorage.h" +#include +#include + +typedef RWStorage SharedIconCache; +typedef std::shared_ptr SharedIconCachePtr; + +class ThumbnailingResult : public QObject +{ + Q_OBJECT +public slots: + inline void emitResultsReady(const QString &path) { emit resultsReady(path); } + inline void emitResultsFailed(const QString &path) { emit resultsFailed(path); } +signals: + void resultsReady(const QString &path); + void resultsFailed(const QString &path); +}; + +class ThumbnailRunnable : public QRunnable +{ +public: + ThumbnailRunnable(QString path, SharedIconCachePtr cache) + { + m_path = path; + m_cache = cache; + } + void run() + { + QFileInfo info(m_path); + if (info.isDir()) + return; + if ((info.suffix().compare("png", Qt::CaseInsensitive) != 0)) + return; + int tries = 5; + while (tries) + { + if (!m_cache->stale(m_path)) + return; + QImage image(m_path); + if (image.isNull()) + { + QThread::msleep(500); + tries--; + continue; + } + QImage small; + if (image.width() > image.height()) + small = image.scaledToWidth(512).scaledToWidth(256, Qt::SmoothTransformation); + else + small = image.scaledToHeight(512).scaledToHeight(256, Qt::SmoothTransformation); + QPoint offset((256 - small.width()) / 2, (256 - small.height()) / 2); + QImage square(QSize(256, 256), QImage::Format_ARGB32); + square.fill(Qt::transparent); + + QPainter painter(&square); + painter.drawImage(offset, small); + painter.end(); + + QIcon icon(QPixmap::fromImage(square)); + m_cache->add(m_path, icon); + m_resultEmitter.emitResultsReady(m_path); + return; + } + m_resultEmitter.emitResultsFailed(m_path); + } + QString m_path; + SharedIconCachePtr m_cache; + ThumbnailingResult m_resultEmitter; +}; + +// this is about as elegant and well written as a bag of bricks with scribbles done by insane +// asylum patients. +class FilterModel : public QIdentityProxyModel +{ + Q_OBJECT +public: + explicit FilterModel(QObject *parent = 0) : QIdentityProxyModel(parent) + { + m_thumbnailingPool.setMaxThreadCount(4); + m_thumbnailCache = std::make_shared(); + m_thumbnailCache->add("placeholder", APPLICATION->getThemedIcon("screenshot-placeholder")); + connect(&watcher, SIGNAL(fileChanged(QString)), SLOT(fileChanged(QString))); + // FIXME: the watched file set is not updated when files are removed + } + virtual ~FilterModel() { m_thumbnailingPool.waitForDone(500); } + virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const + { + auto model = sourceModel(); + if (!model) + return QVariant(); + if (role == Qt::DisplayRole || role == Qt::EditRole) + { + QVariant result = sourceModel()->data(mapToSource(proxyIndex), role); + return result.toString().remove(QRegExp("\\.png$")); + } + if (role == Qt::DecorationRole) + { + QVariant result = + sourceModel()->data(mapToSource(proxyIndex), QFileSystemModel::FilePathRole); + QString filePath = result.toString(); + QIcon temp; + if (!watched.contains(filePath)) + { + ((QFileSystemWatcher &)watcher).addPath(filePath); + ((QSet &)watched).insert(filePath); + } + if (m_thumbnailCache->get(filePath, temp)) + { + return temp; + } + if (!m_failed.contains(filePath)) + { + ((FilterModel *)this)->thumbnailImage(filePath); + } + return (m_thumbnailCache->get("placeholder")); + } + return sourceModel()->data(mapToSource(proxyIndex), role); + } + virtual bool setData(const QModelIndex &index, const QVariant &value, + int role = Qt::EditRole) + { + auto model = sourceModel(); + if (!model) + return false; + if (role != Qt::EditRole) + return false; + // FIXME: this is a workaround for a bug in QFileSystemModel, where it doesn't + // sort after renames + { + ((QFileSystemModel *)model)->setNameFilterDisables(true); + ((QFileSystemModel *)model)->setNameFilterDisables(false); + } + return model->setData(mapToSource(index), value.toString() + ".png", role); + } + +private: + void thumbnailImage(QString path) + { + auto runnable = new ThumbnailRunnable(path, m_thumbnailCache); + connect(&(runnable->m_resultEmitter), SIGNAL(resultsReady(QString)), + SLOT(thumbnailReady(QString))); + connect(&(runnable->m_resultEmitter), SIGNAL(resultsFailed(QString)), + SLOT(thumbnailFailed(QString))); + ((QThreadPool &)m_thumbnailingPool).start(runnable); + } +private slots: + void thumbnailReady(QString path) { emit layoutChanged(); } + void thumbnailFailed(QString path) { m_failed.insert(path); } + void fileChanged(QString filepath) + { + m_thumbnailCache->setStale(filepath); + thumbnailImage(filepath); + // reinsert the path... + watcher.removePath(filepath); + watcher.addPath(filepath); + } + +private: + SharedIconCachePtr m_thumbnailCache; + QThreadPool m_thumbnailingPool; + QSet m_failed; + QSet watched; + QFileSystemWatcher watcher; +}; + +class CenteredEditingDelegate : public QStyledItemDelegate +{ +public: + explicit CenteredEditingDelegate(QObject *parent = 0) : QStyledItemDelegate(parent) {} + virtual ~CenteredEditingDelegate() {} + virtual QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, + const QModelIndex &index) const + { + auto widget = QStyledItemDelegate::createEditor(parent, option, index); + auto foo = dynamic_cast(widget); + if (foo) + { + foo->setAlignment(Qt::AlignHCenter); + foo->setFrame(true); + foo->setMaximumWidth(192); + } + return widget; + } +}; + +ScreenshotsPage::ScreenshotsPage(QString path, QWidget *parent) + : QMainWindow(parent), ui(new Ui::ScreenshotsPage) +{ + m_model.reset(new QFileSystemModel()); + m_filterModel.reset(new FilterModel()); + m_filterModel->setSourceModel(m_model.get()); + m_model->setFilter(QDir::Files | QDir::Writable | QDir::Readable); + m_model->setReadOnly(false); + m_model->setNameFilters({"*.png"}); + m_model->setNameFilterDisables(false); + m_folder = path; + m_valid = FS::ensureFolderPathExists(m_folder); + + ui->setupUi(this); + ui->toolBar->insertSpacer(ui->actionView_Folder); + + ui->listView->setIconSize(QSize(128, 128)); + ui->listView->setGridSize(QSize(192, 160)); + ui->listView->setSpacing(9); + // ui->listView->setUniformItemSizes(true); + ui->listView->setLayoutMode(QListView::Batched); + ui->listView->setViewMode(QListView::IconMode); + ui->listView->setResizeMode(QListView::Adjust); + ui->listView->installEventFilter(this); + ui->listView->setEditTriggers(0); + ui->listView->setItemDelegate(new CenteredEditingDelegate(this)); + ui->listView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->listView, &QListView::customContextMenuRequested, this, &ScreenshotsPage::ShowContextMenu); + connect(ui->listView, SIGNAL(activated(QModelIndex)), SLOT(onItemActivated(QModelIndex))); +} + +bool ScreenshotsPage::eventFilter(QObject *obj, QEvent *evt) +{ + if (obj != ui->listView) + return QWidget::eventFilter(obj, evt); + if (evt->type() != QEvent::KeyPress) + { + return QWidget::eventFilter(obj, evt); + } + QKeyEvent *keyEvent = static_cast(evt); + + if (keyEvent->matches(QKeySequence::Copy)) { + on_actionCopy_File_s_triggered(); + return true; + } + + switch (keyEvent->key()) + { + case Qt::Key_Delete: + on_actionDelete_triggered(); + return true; + case Qt::Key_F2: + on_actionRename_triggered(); + return true; + default: + break; + } + return QWidget::eventFilter(obj, evt); +} + +ScreenshotsPage::~ScreenshotsPage() +{ + delete ui; +} + +void ScreenshotsPage::ShowContextMenu(const QPoint& pos) +{ + auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); + + if (ui->listView->selectionModel()->selectedRows().size() > 1) { + menu->removeAction( ui->actionCopy_Image ); + } + + menu->exec(ui->listView->mapToGlobal(pos)); + delete menu; +} + +QMenu * ScreenshotsPage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction( ui->toolBar->toggleViewAction() ); + return filteredMenu; +} + +void ScreenshotsPage::onItemActivated(QModelIndex index) +{ + if (!index.isValid()) + return; + auto info = m_model->fileInfo(index); + QString fileName = info.absoluteFilePath(); + DesktopServices::openFile(info.absoluteFilePath()); +} + +void ScreenshotsPage::on_actionView_Folder_triggered() +{ + DesktopServices::openDirectory(m_folder, true); +} + +void ScreenshotsPage::on_actionUpload_triggered() +{ + auto selection = ui->listView->selectionModel()->selectedRows(); + if (selection.isEmpty()) + return; + + auto uploadText = tr("Upload screenshot to imgur.com?"); + if (selection.size() > 1) + uploadText = tr("Upload %1 screenshots to imgur.com?").arg(selection.size()); + + auto response = CustomMessageBox::selectable( + this, + tr("Upload?"), + uploadText, + QMessageBox::Question, + QMessageBox::Yes | QMessageBox::No + )->exec(); + if (response == QMessageBox::No) + return; + + QList uploaded; + auto job = NetJob::Ptr(new NetJob("Screenshot Upload", APPLICATION->network())); + if(selection.size() < 2) + { + auto item = selection.at(0); + auto info = m_model->fileInfo(item); + auto screenshot = std::make_shared(info); + job->addNetAction(ImgurUpload::make(screenshot)); + + m_uploadActive = true; + ProgressDialog dialog(this); + + if(dialog.execWithTask(job.get()) != QDialog::Accepted) + { + CustomMessageBox::selectable(this, tr("Failed to upload screenshots!"), + tr("Unknown error"), QMessageBox::Warning)->exec(); + } + else + { + auto link = screenshot->m_url; + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(link); + CustomMessageBox::selectable( + this, + tr("Upload finished"), + tr("The link to the uploaded screenshot has been placed in your clipboard.") + .arg(link), + QMessageBox::Information + )->exec(); + } + + m_uploadActive = false; + return; + } + + for (auto item : selection) + { + auto info = m_model->fileInfo(item); + auto screenshot = std::make_shared(info); + uploaded.push_back(screenshot); + job->addNetAction(ImgurUpload::make(screenshot)); + } + SequentialTask task; + auto albumTask = NetJob::Ptr(new NetJob("Imgur Album Creation", APPLICATION->network())); + auto imgurAlbum = ImgurAlbumCreation::make(uploaded); + albumTask->addNetAction(imgurAlbum); + task.addTask(job); + task.addTask(albumTask); + m_uploadActive = true; + ProgressDialog prog(this); + if (prog.execWithTask(&task) != QDialog::Accepted) + { + CustomMessageBox::selectable( + this, + tr("Failed to upload screenshots!"), + tr("Unknown error"), + QMessageBox::Warning + )->exec(); + } + else + { + auto link = QString("https://imgur.com/a/%1").arg(imgurAlbum->id()); + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(link); + CustomMessageBox::selectable( + this, + tr("Upload finished"), + tr("The link to the uploaded album has been placed in your clipboard.") .arg(link), + QMessageBox::Information + )->exec(); + } + m_uploadActive = false; +} + +void ScreenshotsPage::on_actionCopy_Image_triggered() +{ + auto selection = ui->listView->selectionModel()->selectedRows(); + if(selection.size() < 1) + { + return; + } + + // You can only copy one image to the clipboard. In the case of multiple selected files, only the first one gets copied. + auto item = selection[0]; + auto info = m_model->fileInfo(item); + QImage image(info.absoluteFilePath()); + Q_ASSERT(!image.isNull()); + QApplication::clipboard()->setImage(image, QClipboard::Clipboard); +} + +void ScreenshotsPage::on_actionCopy_File_s_triggered() +{ + auto selection = ui->listView->selectionModel()->selectedRows(); + if(selection.size() < 1) + { + // Don't do anything so we don't empty the users clipboard + return; + } + + QString buf = ""; + for (auto item : selection) + { + auto info = m_model->fileInfo(item); + buf += "file:///" + info.absoluteFilePath() + "\r\n"; + } + QMimeData* mimeData = new QMimeData(); + mimeData->setData("text/uri-list", buf.toLocal8Bit()); + QApplication::clipboard()->setMimeData(mimeData); +} + +void ScreenshotsPage::on_actionDelete_triggered() +{ + auto mbox = CustomMessageBox::selectable( + this, tr("Are you sure?"), tr("This will delete all selected screenshots."), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No); + std::unique_ptr box(mbox); + + if (box->exec() != QMessageBox::Yes) + return; + + auto selected = ui->listView->selectionModel()->selectedIndexes(); + for (auto item : selected) + { + m_model->remove(item); + } +} + +void ScreenshotsPage::on_actionRename_triggered() +{ + auto selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.isEmpty()) + return; + ui->listView->edit(selection[0]); + // TODO: mass renaming +} + +void ScreenshotsPage::openedImpl() +{ + if(!m_valid) + { + m_valid = FS::ensureFolderPathExists(m_folder); + } + if (m_valid) + { + QString path = QDir(m_folder).absolutePath(); + auto idx = m_model->setRootPath(path); + if(idx.isValid()) + { + ui->listView->setModel(m_filterModel.get()); + ui->listView->setRootIndex(m_filterModel->mapFromSource(idx)); + } + else + { + ui->listView->setModel(nullptr); + } + } +} + +#include "ScreenshotsPage.moc" diff --git a/ultimmc/launcher/ui/pages/instance/ScreenshotsPage.h b/ultimmc/launcher/ui/pages/instance/ScreenshotsPage.h new file mode 100644 index 0000000..2a1fdee --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/ScreenshotsPage.h @@ -0,0 +1,91 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "ui/pages/BasePage.h" +#include + +class QFileSystemModel; +class QIdentityProxyModel; +namespace Ui +{ +class ScreenshotsPage; +} + +struct ScreenShot; +class ScreenshotList; +class ImgurAlbumCreation; + +class ScreenshotsPage : public QMainWindow, public BasePage +{ + Q_OBJECT + +public: + explicit ScreenshotsPage(QString path, QWidget *parent = 0); + virtual ~ScreenshotsPage(); + + virtual void openedImpl() override; + + enum + { + NothingDone = 0x42 + }; + + virtual bool eventFilter(QObject *, QEvent *) override; + virtual QString displayName() const override + { + return tr("Screenshots"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("screenshots"); + } + virtual QString id() const override + { + return "screenshots"; + } + virtual QString helpPage() const override + { + return "Screenshots-management"; + } + virtual bool apply() override + { + return !m_uploadActive; + } + +protected: + QMenu * createPopupMenu() override; + +private slots: + void on_actionUpload_triggered(); + void on_actionCopy_Image_triggered(); + void on_actionCopy_File_s_triggered(); + void on_actionDelete_triggered(); + void on_actionRename_triggered(); + void on_actionView_Folder_triggered(); + void onItemActivated(QModelIndex); + void ShowContextMenu(const QPoint &pos); + +private: + Ui::ScreenshotsPage *ui; + std::shared_ptr m_model; + std::shared_ptr m_filterModel; + QString m_folder; + bool m_valid = false; + bool m_uploadActive = false; +}; diff --git a/ultimmc/launcher/ui/pages/instance/ScreenshotsPage.ui b/ultimmc/launcher/ui/pages/instance/ScreenshotsPage.ui new file mode 100644 index 0000000..2e2227a --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/ScreenshotsPage.ui @@ -0,0 +1,105 @@ + + + ScreenshotsPage + + + + 0 + 0 + 800 + 600 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + + + + + + Actions + + + Qt::ToolButtonTextOnly + + + RightToolBarArea + + + false + + + + + + + + + + + Upload + + + + + Delete + + + + + Rename + + + + + View Folder + + + + + Copy Image + + + Copy Image + + + + + Copy File(s) + + + Copy File(s) + + + + + + WideBar + QToolBar +
ui/widgets/WideBar.h
+
+
+ + +
diff --git a/ultimmc/launcher/ui/pages/instance/ServersPage.cpp b/ultimmc/launcher/ui/pages/instance/ServersPage.cpp new file mode 100644 index 0000000..7cd3918 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/ServersPage.cpp @@ -0,0 +1,769 @@ +#include "ServersPage.h" +#include "ui_ServersPage.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static const int COLUMN_COUNT = 2; // 3 , TBD: latency and other nice things. + +struct Server +{ + // Types + enum class AcceptsTextures : int + { + ASK = 0, + ALWAYS = 1, + NEVER = 2 + }; + + // Methods + Server() + { + m_name = QObject::tr("Minecraft Server"); + } + Server(const QString & name, const QString & address) + { + m_name = name; + m_address = address; + } + Server(nbt::tag_compound& server) + { + std::string addressStr(server["ip"]); + m_address = QString::fromUtf8(addressStr.c_str()); + + std::string nameStr(server["name"]); + m_name = QString::fromUtf8(nameStr.c_str()); + + if(server["icon"]) + { + std::string base64str(server["icon"]); + m_icon = QByteArray::fromBase64(base64str.c_str()); + } + + if(server.has_key("acceptTextures", nbt::tag_type::Byte)) + { + bool value = server["acceptTextures"].as().get(); + if(value) + { + m_acceptsTextures = AcceptsTextures::ALWAYS; + } + else + { + m_acceptsTextures = AcceptsTextures::NEVER; + } + } + } + + void serialize(nbt::tag_compound& server) + { + server.insert("name", m_name.trimmed().toUtf8().toStdString()); + server.insert("ip", m_address.trimmed().toUtf8().toStdString()); + if(m_icon.size()) + { + server.insert("icon", m_icon.toBase64().toStdString()); + } + if(m_acceptsTextures != AcceptsTextures::ASK) + { + server.insert("acceptTextures", nbt::tag_byte(m_acceptsTextures == AcceptsTextures::ALWAYS)); + } + } + + // Data - persistent and user changeable + QString m_name; + QString m_address; + AcceptsTextures m_acceptsTextures = AcceptsTextures::ASK; + + // Data - persistent and automatically updated + QByteArray m_icon; + + // Data - temporary + bool m_checked = false; + bool m_up = false; + QString m_motd; // https://mctools.org/motd-creator + int m_ping = 0; + int m_currentPlayers = 0; + int m_maxPlayers = 0; +}; + +static std::unique_ptr parseServersDat(const QString& filename) +{ + try + { + QByteArray input = FS::read(filename); + std::istringstream foo(std::string(input.constData(), input.size())); + auto pair = nbt::io::read_compound(foo); + + if(pair.first != "") + return nullptr; + + if(pair.second == nullptr) + return nullptr; + + return std::move(pair.second); + } + catch (...) + { + return nullptr; + } +} + +static bool serializeServerDat(const QString& filename, nbt::tag_compound * levelInfo) +{ + try + { + if(!FS::ensureFilePathExists(filename)) + { + return false; + } + std::ostringstream s; + nbt::io::write_tag("", *levelInfo, s); + QByteArray val(s.str().data(), (int) s.str().size() ); + FS::write(filename, val); + return true; + } + catch (...) + { + return false; + } +} + +class ServersModel: public QAbstractListModel +{ + Q_OBJECT +public: + enum Roles + { + ServerPtrRole = Qt::UserRole, + }; + explicit ServersModel(const QString &path, QObject *parent = 0) + : QAbstractListModel(parent) + { + m_path = path; + m_watcher = new QFileSystemWatcher(this); + connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ServersModel::fileChanged); + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &ServersModel::dirChanged); + m_saveTimer.setSingleShot(true); + m_saveTimer.setInterval(5000); + connect(&m_saveTimer, &QTimer::timeout, this, &ServersModel::save_internal); + } + virtual ~ServersModel() {}; + + void observe() + { + if(m_observed) + { + return; + } + m_observed = true; + + if(!m_loaded) + { + load(); + } + + updateFSObserver(); + } + + void unobserve() + { + if(!m_observed) + { + return; + } + m_observed = false; + + updateFSObserver(); + } + + void lock() + { + if(m_locked) + { + return; + } + saveNow(); + + m_locked = true; + updateFSObserver(); + } + + void unlock() + { + if(!m_locked) + { + return; + } + m_locked = false; + + updateFSObserver(); + } + + int addEmptyRow(int position) + { + if(m_locked) + { + return -1; + } + if(position < 0 || position >= rowCount()) + { + position = rowCount(); + } + beginInsertRows(QModelIndex(), position, position); + m_servers.insert(position, Server()); + endInsertRows(); + scheduleSave(); + return position; + } + + bool removeRow(int row) + { + if(m_locked) + { + return false; + } + if(row < 0 || row >= rowCount()) + { + return false; + } + beginRemoveRows(QModelIndex(), row, row); + m_servers.removeAt(row); + endRemoveRows(); // does absolutely nothing, the selected server stays as the next line... + scheduleSave(); + return true; + } + + bool moveUp(int row) + { + if(m_locked) + { + return false; + } + if(row <= 0) + { + return false; + } + beginMoveRows(QModelIndex(), row, row, QModelIndex(), row - 1); + m_servers.swap(row-1, row); + endMoveRows(); + scheduleSave(); + return true; + } + + bool moveDown(int row) + { + if(m_locked) + { + return false; + } + int count = rowCount(); + if(row + 1 >= count) + { + return false; + } + beginMoveRows(QModelIndex(), row, row, QModelIndex(), row + 2); + m_servers.swap(row+1, row); + endMoveRows(); + scheduleSave(); + return true; + } + + QVariant headerData(int section, Qt::Orientation orientation, int role) const override + { + if (section < 0 || section >= COLUMN_COUNT) + return QVariant(); + + if(role == Qt::DisplayRole) + { + switch(section) + { + case 0: + return tr("Name"); + case 1: + return tr("Address"); + case 2: + return tr("Latency"); + } + } + + return QAbstractListModel::headerData(section, orientation, role); + } + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override + { + if (!index.isValid()) + return QVariant(); + + int row = index.row(); + int column = index.column(); + if(column < 0 || column >= COLUMN_COUNT) + return QVariant(); + + if (row < 0 || row >= m_servers.size()) + return QVariant(); + + switch(column) + { + case 0: + switch (role) + { + case Qt::DecorationRole: + { + auto & bytes = m_servers[row].m_icon; + if(bytes.size()) + { + QPixmap px; + if(px.loadFromData(bytes)) + return QIcon(px); + } + return APPLICATION->getThemedIcon("unknown_server"); + } + case Qt::DisplayRole: + return m_servers[row].m_name; + case ServerPtrRole: + return QVariant::fromValue((void *)&m_servers[row]); + default: + return QVariant(); + } + case 1: + switch (role) + { + case Qt::DisplayRole: + return m_servers[row].m_address; + default: + return QVariant(); + } + case 2: + switch (role) + { + case Qt::DisplayRole: + return m_servers[row].m_ping; + default: + return QVariant(); + } + default: + return QVariant(); + } + } + + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override + { + return m_servers.size(); + } + int columnCount(const QModelIndex & parent) const override + { + return COLUMN_COUNT; + } + + Server * at(int index) + { + if(index < 0 || index >= rowCount()) + { + return nullptr; + } + return &m_servers[index]; + } + + void setName(int row, const QString & name) + { + if(m_locked) + { + return; + } + auto server = at(row); + if(!server || server->m_name == name) + { + return; + } + server->m_name = name; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + scheduleSave(); + } + + void setAddress(int row, const QString & address) + { + if(m_locked) + { + return; + } + auto server = at(row); + if(!server || server->m_address == address) + { + return; + } + server->m_address = address; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + scheduleSave(); + } + + void setAcceptsTextures(int row, Server::AcceptsTextures textures) + { + if(m_locked) + { + return; + } + auto server = at(row); + if(!server || server->m_acceptsTextures == textures) + { + return; + } + server->m_acceptsTextures = textures; + emit dataChanged(index(row, 0), index(row, COLUMN_COUNT - 1)); + scheduleSave(); + } + + void load() + { + cancelSave(); + beginResetModel(); + QList servers; + auto serversDat = parseServersDat(serversPath()); + if(serversDat) + { + auto &serversList = serversDat->at("servers").as(); + for(auto iter = serversList.begin(); iter != serversList.end(); iter++) + { + auto & serverTag = (*iter).as(); + Server s(serverTag); + servers.append(s); + } + } + m_servers.swap(servers); + m_loaded = true; + endResetModel(); + } + + void saveNow() + { + if(saveIsScheduled()) + { + save_internal(); + } + } + + +public slots: + void dirChanged(const QString& path) + { + qDebug() << "Changed:" << path; + load(); + } + void fileChanged(const QString& path) + { + qDebug() << "Changed:" << path; + } + +private slots: + void save_internal() + { + cancelSave(); + QString path = serversPath(); + qDebug() << "Server list about to be saved to" << path; + + nbt::tag_compound out; + nbt::tag_list list; + for(auto & server: m_servers) + { + nbt::tag_compound serverNbt; + server.serialize(serverNbt); + list.push_back(std::move(serverNbt)); + } + out.insert("servers", nbt::value(std::move(list))); + + if(!serializeServerDat(path, &out)) + { + qDebug() << "Failed to save server list:" << path << "Will try again."; + scheduleSave(); + } + } + +private: + void scheduleSave() + { + if(!m_loaded) + { + qDebug() << "Server list should never save if it didn't successfully load, path:" << m_path; + return; + } + if(!m_dirty) + { + m_dirty = true; + qDebug() << "Server list save is scheduled for" << m_path; + } + m_saveTimer.start(); + } + + void cancelSave() + { + m_dirty = false; + m_saveTimer.stop(); + } + + bool saveIsScheduled() const + { + return m_dirty; + } + + void updateFSObserver() + { + bool observingFS = m_watcher->directories().contains(m_path); + if(m_observed && m_locked) + { + if(!observingFS) + { + qWarning() << "Will watch" << m_path; + if(!m_watcher->addPath(m_path)) + { + qWarning() << "Failed to start watching" << m_path; + } + } + } + else + { + if(observingFS) + { + qWarning() << "Will stop watching" << m_path; + if(!m_watcher->removePath(m_path)) + { + qWarning() << "Failed to stop watching" << m_path; + } + } + } + } + + QString serversPath() + { + QFileInfo foo(FS::PathCombine(m_path, "servers.dat")); + return foo.filePath(); + } + +private: + bool m_loaded = false; + bool m_locked = false; + bool m_observed = false; + bool m_dirty = false; + QString m_path; + QList m_servers; + QFileSystemWatcher *m_watcher = nullptr; + QTimer m_saveTimer; +}; + +ServersPage::ServersPage(InstancePtr inst, QWidget* parent) + : QMainWindow(parent), ui(new Ui::ServersPage) +{ + ui->setupUi(this); + m_inst = inst; + m_model = new ServersModel(inst->gameRoot(), this); + ui->serversView->setIconSize(QSize(64,64)); + ui->serversView->setModel(m_model); + ui->serversView->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->serversView, &QTreeView::customContextMenuRequested, this, &ServersPage::ShowContextMenu); + + auto head = ui->serversView->header(); + if(head->count()) + { + head->setSectionResizeMode(0, QHeaderView::Stretch); + for(int i = 1; i < head->count(); i++) + { + head->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + } + + auto selectionModel = ui->serversView->selectionModel(); + connect(selectionModel, &QItemSelectionModel::currentChanged, this, &ServersPage::currentChanged); + connect(m_inst.get(), &MinecraftInstance::runningStatusChanged, this, &ServersPage::on_RunningState_changed); + connect(ui->nameLine, &QLineEdit::textEdited, this, &ServersPage::nameEdited); + connect(ui->addressLine, &QLineEdit::textEdited, this, &ServersPage::addressEdited); + connect(ui->resourceComboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(resourceIndexChanged(int))); + connect(m_model, &QAbstractItemModel::rowsRemoved, this, &ServersPage::rowsRemoved); + + m_locked = m_inst->isRunning(); + if(m_locked) + { + m_model->lock(); + } + + updateState(); +} + +ServersPage::~ServersPage() +{ + m_model->saveNow(); + delete ui; +} + +void ServersPage::ShowContextMenu(const QPoint& pos) +{ + auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); + menu->exec(ui->serversView->mapToGlobal(pos)); + delete menu; +} + +QMenu * ServersPage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction( ui->toolBar->toggleViewAction() ); + return filteredMenu; +} + +void ServersPage::on_RunningState_changed(bool running) +{ + if(m_locked == running) + { + return; + } + m_locked = running; + if(m_locked) + { + m_model->lock(); + } + else + { + m_model->unlock(); + } + updateState(); +} + +void ServersPage::currentChanged(const QModelIndex ¤t, const QModelIndex &previous) +{ + int nextServer = -1; + if (!current.isValid()) + { + nextServer = -1; + } + else + { + nextServer = current.row(); + } + currentServer = nextServer; + updateState(); +} + +// WARNING: this is here because currentChanged is not accurate when removing rows. the current item needs to be fixed up after removal. +void ServersPage::rowsRemoved(const QModelIndex& parent, int first, int last) +{ + if(currentServer < first) + { + // current was before the removal + return; + } + else if(currentServer >= first && currentServer <= last) + { + // current got removed... + return; + } + else + { + // current was past the removal + int count = last - first + 1; + currentServer -= count; + } +} + +void ServersPage::nameEdited(const QString& name) +{ + m_model->setName(currentServer, name); +} + +void ServersPage::addressEdited(const QString& address) +{ + m_model->setAddress(currentServer, address); +} + +void ServersPage::resourceIndexChanged(int index) +{ + auto acceptsTextures = Server::AcceptsTextures(index); + m_model->setAcceptsTextures(currentServer, acceptsTextures); +} + +void ServersPage::updateState() +{ + auto server = m_model->at(currentServer); + + bool serverEditEnabled = server && !m_locked; + ui->addressLine->setEnabled(serverEditEnabled); + ui->nameLine->setEnabled(serverEditEnabled); + ui->resourceComboBox->setEnabled(serverEditEnabled); + ui->actionMove_Down->setEnabled(serverEditEnabled); + ui->actionMove_Up->setEnabled(serverEditEnabled); + ui->actionRemove->setEnabled(serverEditEnabled); + ui->actionJoin->setEnabled(serverEditEnabled); + + if(server) + { + ui->addressLine->setText(server->m_address); + ui->nameLine->setText(server->m_name); + ui->resourceComboBox->setCurrentIndex(int(server->m_acceptsTextures)); + } + else + { + ui->addressLine->setText(QString()); + ui->nameLine->setText(QString()); + ui->resourceComboBox->setCurrentIndex(0); + } + + ui->actionAdd->setDisabled(m_locked); +} + +void ServersPage::openedImpl() +{ + m_model->observe(); +} + +void ServersPage::closedImpl() +{ + m_model->unobserve(); +} + +void ServersPage::on_actionAdd_triggered() +{ + int position = m_model->addEmptyRow(currentServer + 1); + if(position < 0) + { + return; + } + // select the new row + ui->serversView->selectionModel()->setCurrentIndex( + m_model->index(position), + QItemSelectionModel::SelectCurrent | QItemSelectionModel::Clear | QItemSelectionModel::Rows + ); + currentServer = position; +} + +void ServersPage::on_actionRemove_triggered() +{ + m_model->removeRow(currentServer); +} + +void ServersPage::on_actionMove_Up_triggered() +{ + if(m_model->moveUp(currentServer)) + { + currentServer --; + } +} + +void ServersPage::on_actionMove_Down_triggered() +{ + if(m_model->moveDown(currentServer)) + { + currentServer ++; + } +} + +void ServersPage::on_actionJoin_triggered() +{ + const auto &address = m_model->at(currentServer)->m_address; + APPLICATION->launch(m_inst, true, nullptr, std::make_shared( + QuickPlayTarget::parseMultiplayer(address))); +} + +#include "ServersPage.moc" diff --git a/ultimmc/launcher/ui/pages/instance/ServersPage.h b/ultimmc/launcher/ui/pages/instance/ServersPage.h new file mode 100644 index 0000000..d91da2a --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/ServersPage.h @@ -0,0 +1,94 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "ui/pages/BasePage.h" +#include + +namespace Ui +{ +class ServersPage; +} + +struct Server; +class ServersModel; +class MinecraftInstance; + +class ServersPage : public QMainWindow, public BasePage +{ + Q_OBJECT + +public: + explicit ServersPage(InstancePtr inst, QWidget *parent = 0); + virtual ~ServersPage(); + + void openedImpl() override; + void closedImpl() override; + + virtual QString displayName() const override + { + return tr("Servers"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("unknown_server"); + } + virtual QString id() const override + { + return "servers"; + } + virtual QString helpPage() const override + { + return "Servers-management"; + } + +protected: + QMenu * createPopupMenu() override; + +private: + void updateState(); + void scheduleSave(); + bool saveIsScheduled() const; + +private slots: + void currentChanged(const QModelIndex ¤t, const QModelIndex &previous); + void rowsRemoved(const QModelIndex &parent, int first, int last); + + void on_actionAdd_triggered(); + void on_actionRemove_triggered(); + void on_actionMove_Up_triggered(); + void on_actionMove_Down_triggered(); + void on_actionJoin_triggered(); + + void on_RunningState_changed(bool running); + + void nameEdited(const QString & name); + void addressEdited(const QString & address); + void resourceIndexChanged(int index);\ + + void ShowContextMenu(const QPoint &pos); + +private: // data + int currentServer = -1; + bool m_locked = true; + Ui::ServersPage *ui = nullptr; + ServersModel * m_model = nullptr; + InstancePtr m_inst = nullptr; +}; + diff --git a/ultimmc/launcher/ui/pages/instance/ServersPage.ui b/ultimmc/launcher/ui/pages/instance/ServersPage.ui new file mode 100644 index 0000000..e8f79cf --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/ServersPage.ui @@ -0,0 +1,194 @@ + + + ServersPage + + + + 0 + 0 + 1318 + 879 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + true + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + + 64 + 64 + + + + false + + + false + + + + + + + 6 + + + 6 + + + + + &Name + + + nameLine + + + + + + + + + + Address + + + addressLine + + + + + + + + + + Reso&urces + + + resourceComboBox + + + + + + + + Ask to download + + + + + Always download + + + + + Never download + + + + + + + + + + + Actions + + + Qt::LeftToolBarArea|Qt::RightToolBarArea + + + Qt::ToolButtonTextOnly + + + false + + + RightToolBarArea + + + false + + + + + + + + + + Add + + + + + Remove + + + + + Move Up + + + + + Move Down + + + + + Join + + + + + + WideBar + QToolBar +
ui/widgets/WideBar.h
+
+
+ + serversView + nameLine + addressLine + resourceComboBox + + + +
diff --git a/ultimmc/launcher/ui/pages/instance/ShaderPackPage.h b/ultimmc/launcher/ui/pages/instance/ShaderPackPage.h new file mode 100644 index 0000000..3672499 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/ShaderPackPage.h @@ -0,0 +1,22 @@ +#pragma once + +#include "ModFolderPage.h" +#include "ui_ModFolderPage.h" + +class ShaderPackPage : public ModFolderPage +{ + Q_OBJECT +public: + explicit ShaderPackPage(MinecraftInstance *instance, QWidget *parent = 0) + : ModFolderPage(instance, instance->shaderPackList(), "shaderpacks", + "shaderpacks", tr("Shader packs"), "Resource-packs", parent) + { + ui->actionView_configs->setVisible(false); + } + virtual ~ShaderPackPage() {} + + virtual bool shouldDisplay() const override + { + return true; + } +}; diff --git a/ultimmc/launcher/ui/pages/instance/TexturePackPage.h b/ultimmc/launcher/ui/pages/instance/TexturePackPage.h new file mode 100644 index 0000000..3f04997 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/TexturePackPage.h @@ -0,0 +1,22 @@ +#pragma once + +#include "ModFolderPage.h" +#include "ui_ModFolderPage.h" + +class TexturePackPage : public ModFolderPage +{ + Q_OBJECT +public: + explicit TexturePackPage(MinecraftInstance *instance, QWidget *parent = 0) + : ModFolderPage(instance, instance->texturePackList(), "texturepacks", "resourcepacks", + tr("Texture packs"), "Texture-packs", parent) + { + ui->actionView_configs->setVisible(false); + } + virtual ~TexturePackPage() {} + + virtual bool shouldDisplay() const override + { + return m_inst->traits().contains("texturepacks"); + } +}; diff --git a/ultimmc/launcher/ui/pages/instance/VersionPage.cpp b/ultimmc/launcher/ui/pages/instance/VersionPage.cpp new file mode 100644 index 0000000..0a01611 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/VersionPage.cpp @@ -0,0 +1,704 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Application.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "VersionPage.h" +#include "ui_VersionPage.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/VersionSelectDialog.h" +#include "ui/dialogs/NewComponentDialog.h" +#include "ui/dialogs/ProgressDialog.h" + +#include "ui/GuiUtil.h" + +#include "minecraft/PackProfile.h" +#include "minecraft/auth/AccountList.h" +#include "minecraft/mod/Mod.h" +#include "minecraft/VersionFilterData.h" +#include "icons/IconList.h" +#include "Exception.h" +#include "Version.h" +#include "DesktopServices.h" + +#include "meta/Index.h" +#include "meta/VersionList.h" + +class IconProxy : public QIdentityProxyModel +{ + Q_OBJECT +public: + + IconProxy(QWidget *parentWidget) : QIdentityProxyModel(parentWidget) + { + connect(parentWidget, &QObject::destroyed, this, &IconProxy::widgetGone); + m_parentWidget = parentWidget; + } + + virtual QVariant data(const QModelIndex &proxyIndex, int role = Qt::DisplayRole) const override + { + QVariant var = QIdentityProxyModel::data(proxyIndex, role); + int column = proxyIndex.column(); + if(column == 0 && role == Qt::DecorationRole && m_parentWidget) + { + if(!var.isNull()) + { + auto string = var.toString(); + if(string == "warning") + { + return APPLICATION->getThemedIcon("status-yellow"); + } + else if(string == "error") + { + return APPLICATION->getThemedIcon("status-bad"); + } + } + return APPLICATION->getThemedIcon("status-good"); + } + return var; + } +private slots: + void widgetGone() + { + m_parentWidget = nullptr; + } + +private: + QWidget *m_parentWidget = nullptr; +}; + +QIcon VersionPage::icon() const +{ + return APPLICATION->icons()->getIcon(m_inst->iconKey()); +} +bool VersionPage::shouldDisplay() const +{ + return true; +} + +QMenu * VersionPage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction( ui->toolBar->toggleViewAction() ); + return filteredMenu; +} + +VersionPage::VersionPage(MinecraftInstance *inst, QWidget *parent) + : QMainWindow(parent), ui(new Ui::VersionPage), m_inst(inst) +{ + ui->setupUi(this); + + ui->toolBar->insertSpacer(ui->actionReload); + + m_profile = m_inst->getPackProfile(); + + reloadPackProfile(); + + auto proxy = new IconProxy(ui->packageView); + proxy->setSourceModel(m_profile.get()); + + m_filterModel = new QSortFilterProxyModel(); + m_filterModel->setDynamicSortFilter(true); + m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); + m_filterModel->setSourceModel(proxy); + m_filterModel->setFilterKeyColumn(-1); + + ui->packageView->setModel(m_filterModel); + ui->packageView->installEventFilter(this); + ui->packageView->setSelectionMode(QAbstractItemView::SingleSelection); + ui->packageView->setContextMenuPolicy(Qt::CustomContextMenu); + + connect(ui->packageView->selectionModel(), &QItemSelectionModel::currentChanged, this, &VersionPage::versionCurrent); + auto smodel = ui->packageView->selectionModel(); + connect(smodel, &QItemSelectionModel::currentChanged, this, &VersionPage::packageCurrent); + + connect(m_profile.get(), &PackProfile::minecraftChanged, this, &VersionPage::updateVersionControls); + controlsEnabled = !m_inst->isRunning(); + updateVersionControls(); + preselect(0); + connect(m_inst, &BaseInstance::runningStatusChanged, this, &VersionPage::updateRunningStatus); + connect(ui->packageView, &ModListView::customContextMenuRequested, this, &VersionPage::showContextMenu); + connect(ui->filterEdit, &QLineEdit::textChanged, this, &VersionPage::onFilterTextChanged); +} + +VersionPage::~VersionPage() +{ + delete ui; +} + +void VersionPage::showContextMenu(const QPoint& pos) +{ + auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); + menu->exec(ui->packageView->mapToGlobal(pos)); + delete menu; +} + +void VersionPage::packageCurrent(const QModelIndex ¤t, const QModelIndex &previous) +{ + if (!current.isValid()) + { + ui->frame->clear(); + return; + } + int row = current.row(); + auto patch = m_profile->getComponent(row); + auto severity = patch->getProblemSeverity(); + switch(severity) + { + case ProblemSeverity::Warning: + ui->frame->setModText(tr("%1 possibly has issues.").arg(patch->getName())); + break; + case ProblemSeverity::Error: + ui->frame->setModText(tr("%1 has issues!").arg(patch->getName())); + break; + default: + case ProblemSeverity::None: + ui->frame->clear(); + return; + } + + auto &problems = patch->getProblems(); + QString problemOut; + for (auto &problem: problems) + { + if(problem.m_severity == ProblemSeverity::Error) + { + problemOut += tr("Error: "); + } + else if(problem.m_severity == ProblemSeverity::Warning) + { + problemOut += tr("Warning: "); + } + problemOut += problem.m_description; + problemOut += "\n"; + } + ui->frame->setModDescription(problemOut); +} + +void VersionPage::updateRunningStatus(bool running) +{ + if(controlsEnabled == running) { + controlsEnabled = !running; + updateVersionControls(); + } +} + +void VersionPage::updateVersionControls() +{ + // FIXME: This is better than the broken stuff we had before, but it would probably be better to handle this in meta somehow + auto minecraftReleaseDate = m_profile->getComponent("net.minecraft")->getReleaseDateTime(); + + bool supportsFabric = minecraftReleaseDate >= g_VersionFilterData.fabricBeginsDate; + ui->actionInstall_Fabric->setEnabled(controlsEnabled && supportsFabric); + ui->actionInstall_Quilt->setEnabled((controlsEnabled) && supportsFabric); + + bool supportsLiteLoader = minecraftReleaseDate <= g_VersionFilterData.liteLoaderEndsDate; + ui->actionInstall_LiteLoader->setEnabled(controlsEnabled && supportsLiteLoader); + + bool supportsNeoForge = minecraftReleaseDate >= g_VersionFilterData.neoForgeBeginsDate; + ui->actionInstall_NeoForge->setEnabled(controlsEnabled && supportsNeoForge); + + updateButtons(); +} + +void VersionPage::updateButtons(int row) +{ + if(row == -1) + row = currentRow(); + auto patch = m_profile->getComponent(row); + ui->actionRemove->setEnabled(controlsEnabled && patch && patch->isRemovable()); + ui->actionMove_down->setEnabled(controlsEnabled && patch && patch->isMoveable()); + ui->actionMove_up->setEnabled(controlsEnabled && patch && patch->isMoveable()); + ui->actionChange_version->setEnabled(controlsEnabled && patch && patch->isVersionChangeable()); + ui->actionEdit->setEnabled(controlsEnabled && patch && patch->isCustom()); + ui->actionCustomize->setEnabled(controlsEnabled && patch && patch->isCustomizable()); + ui->actionRevert->setEnabled(controlsEnabled && patch && patch->isRevertible()); + ui->actionDownload_All->setEnabled(controlsEnabled); + ui->actionAdd_Empty->setEnabled(controlsEnabled); + ui->actionReload->setEnabled(controlsEnabled); + ui->actionInstall_mods->setEnabled(controlsEnabled); + ui->actionReplace_Minecraft_jar->setEnabled(controlsEnabled); + ui->actionAdd_to_Minecraft_jar->setEnabled(controlsEnabled); +} + +bool VersionPage::reloadPackProfile() +{ + try + { + m_profile->reload(Net::Mode::Online); + return true; + } + catch (const Exception &e) + { + QMessageBox::critical(this, tr("Error"), e.cause()); + return false; + } + catch (...) + { + QMessageBox::critical( + this, tr("Error"), + tr("Couldn't load the instance profile.")); + return false; + } +} + +void VersionPage::on_actionReload_triggered() +{ + reloadPackProfile(); + m_container->refreshContainer(); +} + +void VersionPage::on_actionRemove_triggered() +{ + if (ui->packageView->currentIndex().isValid()) + { + // FIXME: use actual model, not reloading. + if (!m_profile->remove(ui->packageView->currentIndex().row())) + { + QMessageBox::critical(this, tr("Error"), tr("Couldn't remove file")); + } + } + updateButtons(); + reloadPackProfile(); + m_container->refreshContainer(); +} + +void VersionPage::on_actionInstall_mods_triggered() +{ + if(m_container) + { + m_container->selectPage("mods"); + } +} + +void VersionPage::on_actionAdd_to_Minecraft_jar_triggered() +{ + auto list = GuiUtil::BrowseForFiles("jarmod", tr("Select jar mods"), tr("Minecraft.jar mods (*.zip *.jar)"), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); + if(!list.empty()) + { + m_profile->installJarMods(list); + } + updateButtons(); +} + +void VersionPage::on_actionReplace_Minecraft_jar_triggered() +{ + auto jarPath = GuiUtil::BrowseForFile("jar", tr("Select jar"), tr("Minecraft.jar replacement (*.jar)"), APPLICATION->settings()->get("CentralModsDir").toString(), this->parentWidget()); + if(!jarPath.isEmpty()) + { + m_profile->installCustomJar(jarPath); + } + updateButtons(); +} + +void VersionPage::on_actionMove_up_triggered() +{ + try + { + m_profile->move(currentRow(), PackProfile::MoveUp); + } + catch (const Exception &e) + { + QMessageBox::critical(this, tr("Error"), e.cause()); + } + updateButtons(); +} + +void VersionPage::on_actionMove_down_triggered() +{ + try + { + m_profile->move(currentRow(), PackProfile::MoveDown); + } + catch (const Exception &e) + { + QMessageBox::critical(this, tr("Error"), e.cause()); + } + updateButtons(); +} + +void VersionPage::on_actionChange_version_triggered() +{ + auto versionRow = currentRow(); + if(versionRow == -1) + { + return; + } + auto patch = m_profile->getComponent(versionRow); + auto name = patch->getName(); + auto list = patch->getVersionList(); + if(!list) + { + return; + } + auto uid = list->uid(); + // FIXME: this is a horrible HACK. Get version filtering information from the actual metadata... + if(uid == "net.minecraftforge") + { + on_actionInstall_Forge_triggered(); + return; + } + else if (uid == "net.neoforged") + { + on_actionInstall_NeoForge_triggered(); + return; + } + else if (uid == "com.mumfrey.liteloader") + { + on_actionInstall_LiteLoader_triggered(); + return; + } + VersionSelectDialog vselect(list.get(), tr("Change %1 version").arg(name), this); + if (uid == "net.fabricmc.intermediary" || uid == "org.quiltmc.hashed") + { + vselect.setEmptyString(tr("No intermediary mappings versions are currently available.")); + vselect.setEmptyErrorString(tr("Couldn't load or download the intermediary mappings version lists!")); + vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); + } + auto currentVersion = patch->getVersion(); + if(!currentVersion.isEmpty()) + { + vselect.setCurrentVersion(currentVersion); + } + if (!vselect.exec() || !vselect.selectedVersion()) + return; + + qDebug() << "Change" << uid << "to" << vselect.selectedVersion()->descriptor(); + bool important = false; + if(uid == "net.minecraft") + { + important = true; + } + m_profile->setComponentVersion(uid, vselect.selectedVersion()->descriptor(), important); + m_profile->resolve(Net::Mode::Online); + m_container->refreshContainer(); +} + +void VersionPage::on_actionDownload_All_triggered() +{ + if (!APPLICATION->accounts()->anyAccountIsValid()) + { + CustomMessageBox::selectable( + this, tr("Error"), + tr("MultiMC cannot download Minecraft or update instances unless you have at least " + "one account added.\nPlease add your Mojang or Minecraft account."), + QMessageBox::Warning)->show(); + return; + } + + auto updateTask = m_inst->createUpdateTask(Net::Mode::Online); + if (!updateTask) + { + return; + } + ProgressDialog tDialog(this); + connect(updateTask.get(), SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString))); + // FIXME: unused return value + tDialog.execWithTask(updateTask.get()); + updateButtons(); + m_container->refreshContainer(); +} + +void VersionPage::on_actionInstall_Forge_triggered() +{ + auto vlist = APPLICATION->metadataIndex()->get("net.minecraftforge"); + if(!vlist) + { + return; + } + VersionSelectDialog vselect(vlist.get(), tr("Select Forge version"), this); + vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); + vselect.setEmptyString(tr("No Forge versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft")); + vselect.setEmptyErrorString(tr("Couldn't load or download the Forge version lists!")); + + auto currentVersion = m_profile->getComponentVersion("net.minecraftforge"); + if(!currentVersion.isEmpty()) + { + vselect.setCurrentVersion(currentVersion); + } + + if (vselect.exec() && vselect.selectedVersion()) + { + auto vsn = vselect.selectedVersion(); + m_profile->setComponentVersion("net.minecraftforge", vsn->descriptor()); + m_profile->resolve(Net::Mode::Online); + // m_profile->installVersion(); + preselect(m_profile->rowCount(QModelIndex())-1); + m_container->refreshContainer(); + } +} + +void VersionPage::on_actionInstall_NeoForge_triggered() +{ + auto vlist = APPLICATION->metadataIndex()->get("net.neoforged"); + if(!vlist) + { + return; + } + VersionSelectDialog vselect(vlist.get(), tr("Select NeoForge version"), this); + vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); + vselect.setEmptyString(tr("No NeoForge versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft")); + vselect.setEmptyErrorString(tr("Couldn't load or download the NeoForge version lists!")); + + auto currentVersion = m_profile->getComponentVersion("net.neoforged"); + if(!currentVersion.isEmpty()) + { + vselect.setCurrentVersion(currentVersion); + } + + if (vselect.exec() && vselect.selectedVersion()) + { + auto vsn = vselect.selectedVersion(); + m_profile->setComponentVersion("net.neoforged", vsn->descriptor()); + m_profile->resolve(Net::Mode::Online); + // m_profile->installVersion(); + preselect(m_profile->rowCount(QModelIndex())-1); + m_container->refreshContainer(); + } +} + +void VersionPage::on_actionInstall_Fabric_triggered() +{ + auto vlist = APPLICATION->metadataIndex()->get("net.fabricmc.fabric-loader"); + if(!vlist) + { + return; + } + VersionSelectDialog vselect(vlist.get(), tr("Select Fabric Loader version"), this); + vselect.setEmptyString(tr("No Fabric Loader versions are currently available.")); + vselect.setEmptyErrorString(tr("Couldn't load or download the Fabric Loader version lists!")); + + auto currentVersion = m_profile->getComponentVersion("net.fabricmc.fabric-loader"); + if(!currentVersion.isEmpty()) + { + vselect.setCurrentVersion(currentVersion); + } + + if (vselect.exec() && vselect.selectedVersion()) + { + auto vsn = vselect.selectedVersion(); + m_profile->setComponentVersion("net.fabricmc.fabric-loader", vsn->descriptor()); + m_profile->resolve(Net::Mode::Online); + preselect(m_profile->rowCount(QModelIndex())-1); + m_container->refreshContainer(); + } +} + +void VersionPage::on_actionInstall_Quilt_triggered() +{ + auto vlist = APPLICATION->metadataIndex()->get("org.quiltmc.quilt-loader"); + if(!vlist) + { + return; + } + VersionSelectDialog vselect(vlist.get(), tr("Select Quilt Loader version"), this); + vselect.setEmptyString(tr("No Quilt Loader versions are currently available.")); + vselect.setEmptyErrorString(tr("Couldn't load or download the Quilt Loader version lists!")); + + auto currentVersion = m_profile->getComponentVersion("org.quiltmc.quilt-loader"); + if(!currentVersion.isEmpty()) + { + vselect.setCurrentVersion(currentVersion); + } + + if (vselect.exec() && vselect.selectedVersion()) + { + auto vsn = vselect.selectedVersion(); + m_profile->setComponentVersion("org.quiltmc.quilt-loader", vsn->descriptor()); + m_profile->resolve(Net::Mode::Online); + preselect(m_profile->rowCount(QModelIndex())-1); + m_container->refreshContainer(); + } +} + +void VersionPage::on_actionAdd_Empty_triggered() +{ + NewComponentDialog compdialog(QString(), QString(), this); + QStringList blacklist; + for(int i = 0; i < m_profile->rowCount(); i++) + { + auto comp = m_profile->getComponent(i); + blacklist.push_back(comp->getID()); + } + compdialog.setBlacklist(blacklist); + if (compdialog.exec()) + { + qDebug() << "name:" << compdialog.name(); + qDebug() << "uid:" << compdialog.uid(); + m_profile->installEmpty(compdialog.uid(), compdialog.name()); + } +} + +void VersionPage::on_actionInstall_LiteLoader_triggered() +{ + auto vlist = APPLICATION->metadataIndex()->get("com.mumfrey.liteloader"); + if(!vlist) + { + return; + } + VersionSelectDialog vselect(vlist.get(), tr("Select LiteLoader version"), this); + vselect.setExactFilter(BaseVersionList::ParentVersionRole, m_profile->getComponentVersion("net.minecraft")); + vselect.setEmptyString(tr("No LiteLoader versions are currently available for Minecraft ") + m_profile->getComponentVersion("net.minecraft")); + vselect.setEmptyErrorString(tr("Couldn't load or download the LiteLoader version lists!")); + + auto currentVersion = m_profile->getComponentVersion("com.mumfrey.liteloader"); + if(!currentVersion.isEmpty()) + { + vselect.setCurrentVersion(currentVersion); + } + + if (vselect.exec() && vselect.selectedVersion()) + { + auto vsn = vselect.selectedVersion(); + m_profile->setComponentVersion("com.mumfrey.liteloader", vsn->descriptor()); + m_profile->resolve(Net::Mode::Online); + // m_profile->installVersion(vselect.selectedVersion()); + preselect(m_profile->rowCount(QModelIndex())-1); + m_container->refreshContainer(); + } +} + +void VersionPage::on_actionLibrariesFolder_triggered() +{ + DesktopServices::openDirectory(m_inst->getLocalLibraryPath(), true); +} + +void VersionPage::on_actionMinecraftFolder_triggered() +{ + DesktopServices::openDirectory(m_inst->gameRoot(), true); +} + +void VersionPage::versionCurrent(const QModelIndex ¤t, const QModelIndex &previous) +{ + currentIdx = current.row(); + updateButtons(currentIdx); +} + +void VersionPage::preselect(int row) +{ + if(row < 0) + { + row = 0; + } + if(row >= m_profile->rowCount(QModelIndex())) + { + row = m_profile->rowCount(QModelIndex()) - 1; + } + if(row < 0) + { + return; + } + auto model_index = m_profile->index(row); + ui->packageView->selectionModel()->select(model_index, QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); + updateButtons(row); +} + +void VersionPage::onGameUpdateError(QString error) +{ + CustomMessageBox::selectable(this, tr("Error updating instance"), error, QMessageBox::Warning)->show(); +} + +Component * VersionPage::current() +{ + auto row = currentRow(); + if(row < 0) + { + return nullptr; + } + return m_profile->getComponent(row); +} + +int VersionPage::currentRow() +{ + if (ui->packageView->selectionModel()->selectedRows().isEmpty()) + { + return -1; + } + return ui->packageView->selectionModel()->selectedRows().first().row(); +} + +void VersionPage::on_actionCustomize_triggered() +{ + auto version = currentRow(); + if(version == -1) + { + return; + } + auto patch = m_profile->getComponent(version); + if(!patch->getVersionFile()) + { + // TODO: wait for the update task to finish here... + return; + } + if(!m_profile->customize(version)) + { + // TODO: some error box here + } + updateButtons(); + preselect(currentIdx); +} + +void VersionPage::on_actionEdit_triggered() +{ + auto version = current(); + if(!version) + { + return; + } + auto filename = version->getFilename(); + if(!QFileInfo::exists(filename)) + { + qWarning() << "file" << filename << "can't be opened for editing, doesn't exist!"; + return; + } + APPLICATION->openJsonEditor(filename); +} + +void VersionPage::on_actionRevert_triggered() +{ + auto version = currentRow(); + if(version == -1) + { + return; + } + if(!m_profile->revertToBase(version)) + { + // TODO: some error box here + } + updateButtons(); + preselect(currentIdx); + m_container->refreshContainer(); +} + +void VersionPage::onFilterTextChanged(const QString &newContents) +{ + m_filterModel->setFilterFixedString(newContents); +} + +#include "VersionPage.moc" + diff --git a/ultimmc/launcher/ui/pages/instance/VersionPage.h b/ultimmc/launcher/ui/pages/instance/VersionPage.h new file mode 100644 index 0000000..2f89bea --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/VersionPage.h @@ -0,0 +1,106 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "ui/pages/BasePage.h" + +namespace Ui +{ +class VersionPage; +} + +class VersionPage : public QMainWindow, public BasePage +{ + Q_OBJECT + +public: + explicit VersionPage(MinecraftInstance *inst, QWidget *parent = 0); + virtual ~VersionPage(); + virtual QString displayName() const override + { + return tr("Version"); + } + virtual QIcon icon() const override; + virtual QString id() const override + { + return "version"; + } + virtual QString helpPage() const override + { + return "Instance-Version"; + } + virtual bool shouldDisplay() const override; + +private slots: + void on_actionChange_version_triggered(); + void on_actionInstall_Forge_triggered(); + void on_actionInstall_NeoForge_triggered(); + void on_actionInstall_Fabric_triggered(); + void on_actionInstall_Quilt_triggered(); + void on_actionAdd_Empty_triggered(); + void on_actionInstall_LiteLoader_triggered(); + void on_actionReload_triggered(); + void on_actionRemove_triggered(); + void on_actionMove_up_triggered(); + void on_actionMove_down_triggered(); + void on_actionAdd_to_Minecraft_jar_triggered(); + void on_actionReplace_Minecraft_jar_triggered(); + void on_actionRevert_triggered(); + void on_actionEdit_triggered(); + void on_actionInstall_mods_triggered(); + void on_actionCustomize_triggered(); + void on_actionDownload_All_triggered(); + + void on_actionMinecraftFolder_triggered(); + void on_actionLibrariesFolder_triggered(); + + void updateVersionControls(); + +private: + Component * current(); + int currentRow(); + void updateButtons(int row = -1); + void preselect(int row = 0); + int doUpdate(); + +protected: + QMenu * createPopupMenu() override; + + /// FIXME: this shouldn't be necessary! + bool reloadPackProfile(); + +private: + Ui::VersionPage *ui; + QSortFilterProxyModel *m_filterModel; + std::shared_ptr m_profile; + MinecraftInstance *m_inst; + int currentIdx = 0; + bool controlsEnabled = false; + +public slots: + void versionCurrent(const QModelIndex ¤t, const QModelIndex &previous); + +private slots: + void updateRunningStatus(bool running); + void onGameUpdateError(QString error); + void packageCurrent(const QModelIndex ¤t, const QModelIndex &previous); + void showContextMenu(const QPoint &pos); + void onFilterTextChanged(const QString & newContents); +}; diff --git a/ultimmc/launcher/ui/pages/instance/VersionPage.ui b/ultimmc/launcher/ui/pages/instance/VersionPage.ui new file mode 100644 index 0000000..92f1636 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/VersionPage.ui @@ -0,0 +1,303 @@ + + + VersionPage + + + + 0 + 0 + 961 + 1091 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Qt::ScrollBarAlwaysOn + + + Qt::ScrollBarAlwaysOff + + + false + + + false + + + true + + + + + + + + + true + + + + + + + Filter: + + + + + + + + + + 0 + 0 + + + + + + + + + + + Actions + + + Qt::LeftToolBarArea|Qt::RightToolBarArea + + + Qt::ToolButtonTextOnly + + + false + + + RightToolBarArea + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Change version + + + Change version of the selected package. + + + + + Move up + + + Make the selected package apply sooner. + + + + + Move down + + + Make the selected package apply later. + + + + + Remove + + + Remove selected package from the instance. + + + + + Customize + + + Customize selected package. + + + + + Edit + + + Edit selected package. + + + + + Revert + + + Revert the selected package to default. + + + + + Install Forge + + + Install the Minecraft Forge package. + + + + + Install Fabric + + + Install the Fabric Loader package. + + + + + Install Quilt + + + Install the Quilt Loader package. + + + + + Install LiteLoader + + + Install the LiteLoader package. + + + + + Install mods + + + Install normal mods. + + + + + Add to Minecraft.jar + + + Add a mod into the Minecraft jar file. + + + + + Replace Minecraft.jar + + + + + Add Empty + + + Add an empty custom package. + + + + + Reload + + + Reload all packages. + + + + + Download All + + + Download the files needed to launch the instance now. + + + + + Open .minecraft + + + Open the instance's .minecraft folder. + + + + + Open libraries + + + Open the instance's local libraries folder. + + + + + Install NeoForge + + + Install the NeoForge package. + + + + + + ModListView + QTreeView +
ui/widgets/ModListView.h
+
+ + MCModInfoFrame + QFrame +
ui/widgets/MCModInfoFrame.h
+ 1 +
+ + WideBar + QToolBar +
ui/widgets/WideBar.h
+
+
+ + +
diff --git a/ultimmc/launcher/ui/pages/instance/WorldListPage.cpp b/ultimmc/launcher/ui/pages/instance/WorldListPage.cpp new file mode 100644 index 0000000..5b02e4c --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/WorldListPage.cpp @@ -0,0 +1,446 @@ +/* Copyright 2015-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "WorldListPage.h" +#include "ui_WorldListPage.h" +#include "minecraft/WorldList.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "tools/MCEditTool.h" +#include "FileSystem.h" + +#include "ui/GuiUtil.h" +#include "DesktopServices.h" + +#include "minecraft/PackProfile.h" +#include "minecraft/VersionFilterData.h" + +#include "Application.h" + + +class WorldListProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + WorldListProxyModel(QObject *parent) : QSortFilterProxyModel(parent) {} + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const + { + QModelIndex sourceIndex = mapToSource(index); + + if (index.column() == 0 && role == Qt::DecorationRole) + { + WorldList *worlds = qobject_cast(sourceModel()); + auto iconFile = worlds->data(sourceIndex, WorldList::IconFileRole).toString(); + if(iconFile.isNull()) { + // NOTE: Minecraft uses the same placeholder for servers AND worlds + return APPLICATION->getThemedIcon("unknown_server"); + } + return QIcon(iconFile); + } + + return sourceIndex.data(role); + } +}; + + +WorldListPage::WorldListPage(InstancePtr inst, std::shared_ptr worlds, QWidget *parent) + : QMainWindow(parent), m_inst(inst), ui(new Ui::WorldListPage), m_worlds(worlds) +{ + ui->setupUi(this); + + ui->toolBar->insertSpacer(ui->actionRefresh); + + WorldListProxyModel * proxy = new WorldListProxyModel(this); + proxy->setSortCaseSensitivity(Qt::CaseInsensitive); + proxy->setSourceModel(m_worlds.get()); + ui->worldTreeView->setSortingEnabled(true); + ui->worldTreeView->setModel(proxy); + ui->worldTreeView->installEventFilter(this); + ui->worldTreeView->setContextMenuPolicy(Qt::CustomContextMenu); + ui->worldTreeView->setIconSize(QSize(64,64)); + connect(ui->worldTreeView, &QTreeView::customContextMenuRequested, this, &WorldListPage::ShowContextMenu); + + auto head = ui->worldTreeView->header(); + head->setSectionResizeMode(0, QHeaderView::Stretch); + head->setSectionResizeMode(1, QHeaderView::ResizeToContents); + + connect(ui->worldTreeView->selectionModel(), &QItemSelectionModel::currentChanged, this, &WorldListPage::worldChanged); + worldChanged(QModelIndex(), QModelIndex()); +} + +void WorldListPage::openedImpl() +{ + m_worlds->startWatching(); +} + +void WorldListPage::closedImpl() +{ + m_worlds->stopWatching(); +} + +WorldListPage::~WorldListPage() +{ + m_worlds->stopWatching(); + delete ui; +} + +void WorldListPage::ShowContextMenu(const QPoint& pos) +{ + auto menu = ui->toolBar->createContextMenu(this, tr("Context menu")); + menu->exec(ui->worldTreeView->mapToGlobal(pos)); + delete menu; +} + +QMenu * WorldListPage::createPopupMenu() +{ + QMenu* filteredMenu = QMainWindow::createPopupMenu(); + filteredMenu->removeAction( ui->toolBar->toggleViewAction() ); + return filteredMenu; +} + +bool WorldListPage::shouldDisplay() const +{ + return true; +} + +bool WorldListPage::worldListFilter(QKeyEvent *keyEvent) +{ + switch (keyEvent->key()) + { + case Qt::Key_Delete: + on_actionRemove_triggered(); + return true; + default: + break; + } + return QWidget::eventFilter(ui->worldTreeView, keyEvent); +} + +bool WorldListPage::eventFilter(QObject *obj, QEvent *ev) +{ + if (ev->type() != QEvent::KeyPress) + { + return QWidget::eventFilter(obj, ev); + } + QKeyEvent *keyEvent = static_cast(ev); + if (obj == ui->worldTreeView) + return worldListFilter(keyEvent); + return QWidget::eventFilter(obj, ev); +} + +void WorldListPage::on_actionRemove_triggered() +{ + auto proxiedIndex = getSelectedWorld(); + + if(!proxiedIndex.isValid()) + return; + + auto result = QMessageBox::question(this, + tr("Are you sure?"), + tr("This will remove the selected world permenantly.\n" + "The world will be gone forever (A LONG TIME).\n" + "\n" + "Do you want to continue?")); + if(result != QMessageBox::Yes) + { + return; + } + m_worlds->stopWatching(); + m_worlds->deleteWorld(proxiedIndex.row()); + m_worlds->startWatching(); +} + +void WorldListPage::on_actionView_Folder_triggered() +{ + DesktopServices::openDirectory(m_worlds->dir().absolutePath(), true); +} + +void WorldListPage::on_actionDatapacks_triggered() +{ + QModelIndex index = getSelectedWorld(); + + if (!index.isValid()) + { + return; + } + + if(!worldSafetyNagQuestion()) + return; + + auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); + + DesktopServices::openDirectory(FS::PathCombine(fullPath, "datapacks"), true); +} + + +void WorldListPage::on_actionReset_Icon_triggered() +{ + auto proxiedIndex = getSelectedWorld(); + + if(!proxiedIndex.isValid()) + return; + + if(m_worlds->resetIcon(proxiedIndex.row())) { + ui->actionReset_Icon->setEnabled(false); + } +} + + +QModelIndex WorldListPage::getSelectedWorld() +{ + auto index = ui->worldTreeView->selectionModel()->currentIndex(); + + auto proxy = (QSortFilterProxyModel *) ui->worldTreeView->model(); + return proxy->mapToSource(index); +} + +void WorldListPage::on_actionCopy_Seed_triggered() +{ + QModelIndex index = getSelectedWorld(); + + if (!index.isValid()) + { + return; + } + int64_t seed = m_worlds->data(index, WorldList::SeedRole).toLongLong(); + APPLICATION->clipboard()->setText(QString::number(seed)); +} + +void WorldListPage::on_actionMCEdit_triggered() +{ + if(m_mceditStarting) + return; + + auto mcedit = APPLICATION->mcedit(); + + const QString mceditPath = mcedit->path(); + + QModelIndex index = getSelectedWorld(); + + if (!index.isValid()) + { + return; + } + + if(!worldSafetyNagQuestion()) + return; + + auto fullPath = m_worlds->data(index, WorldList::FolderRole).toString(); + + auto program = mcedit->getProgramPath(); + if(program.size()) + { +#ifdef Q_OS_WIN32 + if(!QProcess::startDetached(program, {fullPath}, mceditPath)) + { + mceditError(); + } +#else + m_mceditProcess.reset(new LoggedProcess()); + m_mceditProcess->setDetachable(true); + connect(m_mceditProcess.get(), &LoggedProcess::stateChanged, this, &WorldListPage::mceditState); + m_mceditProcess->start(program, {fullPath}); + m_mceditProcess->setWorkingDirectory(mceditPath); + m_mceditStarting = true; +#endif + } + else + { + QMessageBox::warning( + this->parentWidget(), + tr("No MCEdit found or set up!"), + tr("You do not have MCEdit set up or it was moved.\nYou can set it up in the global settings.") + ); + } +} + +void WorldListPage::mceditError() +{ + QMessageBox::warning( + this->parentWidget(), + tr("MCEdit failed to start!"), + tr("MCEdit failed to start.\nIt may be necessary to reinstall it.") + ); +} + +void WorldListPage::mceditState(LoggedProcess::State state) +{ + bool failed = false; + switch(state) + { + case LoggedProcess::NotRunning: + case LoggedProcess::Starting: + return; + case LoggedProcess::FailedToStart: + case LoggedProcess::Crashed: + case LoggedProcess::Aborted: + { + failed = true; + } + case LoggedProcess::Running: + case LoggedProcess::Finished: + { + m_mceditStarting = false; + break; + } + } + if(failed) + { + mceditError(); + } +} + +void WorldListPage::worldChanged(const QModelIndex ¤t, const QModelIndex &previous) +{ + auto mcInst = std::dynamic_pointer_cast(m_inst); + bool enableJoinActions = mcInst && mcInst->getPackProfile()->getComponent("net.minecraft")->getReleaseDateTime() >= g_VersionFilterData.quickPlayBeginsDate; + + QModelIndex index = getSelectedWorld(); + bool enable = index.isValid(); + ui->actionJoin->setVisible(enableJoinActions); + ui->actionJoinOffline->setVisible(enableJoinActions); + ui->actionJoin->setEnabled(enable && enableJoinActions); + ui->actionJoinOffline->setEnabled(enable && enableJoinActions); + ui->actionCopy_Seed->setEnabled(enable); + ui->actionMCEdit->setEnabled(enable); + ui->actionRemove->setEnabled(enable); + ui->actionCopy->setEnabled(enable); + ui->actionRename->setEnabled(enable); + ui->actionDatapacks->setEnabled(enable); + bool hasIcon = !index.data(WorldList::IconFileRole).isNull(); + ui->actionReset_Icon->setEnabled(enable && hasIcon); +} + +void WorldListPage::on_actionAdd_triggered() +{ + auto list = GuiUtil::BrowseForFiles( + displayName(), + tr("Select a Minecraft world zip"), + tr("Minecraft World Zip File (*.zip)"), QString(), this->parentWidget()); + if (!list.empty()) + { + m_worlds->stopWatching(); + for (auto filename : list) + { + m_worlds->installWorld(QFileInfo(filename)); + } + m_worlds->startWatching(); + } +} + +bool WorldListPage::isWorldSafe(QModelIndex) +{ + return !m_inst->isRunning(); +} + +bool WorldListPage::worldSafetyNagQuestion() +{ + if(!isWorldSafe(getSelectedWorld())) + { + auto result = QMessageBox::question(this, tr("Copy World"), tr("Changing a world while Minecraft is running is potentially unsafe.\nDo you wish to proceed?")); + if(result == QMessageBox::No) + { + return false; + } + } + return true; +} + + +void WorldListPage::on_actionCopy_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) + { + return; + } + + if(!worldSafetyNagQuestion()) + return; + + auto worldVariant = m_worlds->data(index, WorldList::ObjectRole); + auto world = (World *) worldVariant.value(); + bool ok = false; + QString name = QInputDialog::getText(this, tr("World name"), tr("Enter a new name for the copy."), QLineEdit::Normal, world->name(), &ok); + + if (ok && name.length() > 0) + { + world->install(m_worlds->dir().absolutePath(), name); + } +} + +void WorldListPage::on_actionRename_triggered() +{ + QModelIndex index = getSelectedWorld(); + if (!index.isValid()) + { + return; + } + + if(!worldSafetyNagQuestion()) + return; + + auto worldVariant = m_worlds->data(index, WorldList::ObjectRole); + auto world = (World *) worldVariant.value(); + + bool ok = false; + QString name = QInputDialog::getText(this, tr("World name"), tr("Enter a new world name."), QLineEdit::Normal, world->name(), &ok); + + if (ok && name.length() > 0) + { + world->rename(name); + } +} + +void WorldListPage::on_actionRefresh_triggered() +{ + m_worlds->update(); +} + +void WorldListPage::joinSelectedWorld(bool online) +{ + auto index = getSelectedWorld(); + if (!index.isValid()) + { + return; + } + + auto worldVariant = m_worlds->data(index, WorldList::ObjectRole); + auto world = (World *) worldVariant.value(); + auto name = world->folderName(); + + APPLICATION->launch(m_inst, online, nullptr, std::make_shared(QuickPlayTarget::parseSingleplayer(name))); +} + +void WorldListPage::on_actionJoin_triggered() +{ + joinSelectedWorld(true); +} + +void WorldListPage::on_actionJoinOffline_triggered() +{ + joinSelectedWorld(false); +} + +#include "WorldListPage.moc" diff --git a/ultimmc/launcher/ui/pages/instance/WorldListPage.h b/ultimmc/launcher/ui/pages/instance/WorldListPage.h new file mode 100644 index 0000000..a656cd7 --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/WorldListPage.h @@ -0,0 +1,102 @@ +/* Copyright 2015-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "minecraft/MinecraftInstance.h" +#include "ui/pages/BasePage.h" +#include +#include + +class WorldList; +namespace Ui +{ +class WorldListPage; +} + +class WorldListPage : public QMainWindow, public BasePage +{ + Q_OBJECT + +public: + explicit WorldListPage( + InstancePtr inst, + std::shared_ptr worlds, + QWidget *parent = 0 + ); + virtual ~WorldListPage(); + + virtual QString displayName() const override + { + return tr("Worlds"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("worlds"); + } + virtual QString id() const override + { + return "worlds"; + } + virtual QString helpPage() const override + { + return "Worlds"; + } + virtual bool shouldDisplay() const override; + + virtual void openedImpl() override; + virtual void closedImpl() override; + +protected: + bool eventFilter(QObject *obj, QEvent *ev) override; + bool worldListFilter(QKeyEvent *ev); + QMenu * createPopupMenu() override; + +protected: + InstancePtr m_inst; + +private: + QModelIndex getSelectedWorld(); + bool isWorldSafe(QModelIndex index); + bool worldSafetyNagQuestion(); + void mceditError(); + void joinSelectedWorld(bool online); + +private: + Ui::WorldListPage *ui; + std::shared_ptr m_worlds; + unique_qobject_ptr m_mceditProcess; + bool m_mceditStarting = false; + +private slots: + void on_actionCopy_Seed_triggered(); + void on_actionMCEdit_triggered(); + void on_actionRemove_triggered(); + void on_actionAdd_triggered(); + void on_actionCopy_triggered(); + void on_actionRename_triggered(); + void on_actionRefresh_triggered(); + void on_actionView_Folder_triggered(); + void on_actionDatapacks_triggered(); + void on_actionReset_Icon_triggered(); + void on_actionJoin_triggered(); + void on_actionJoinOffline_triggered(); + void worldChanged(const QModelIndex ¤t, const QModelIndex &previous); + void mceditState(LoggedProcess::State state); + + void ShowContextMenu(const QPoint &pos); +}; diff --git a/ultimmc/launcher/ui/pages/instance/WorldListPage.ui b/ultimmc/launcher/ui/pages/instance/WorldListPage.ui new file mode 100644 index 0000000..c57614e --- /dev/null +++ b/ultimmc/launcher/ui/pages/instance/WorldListPage.ui @@ -0,0 +1,179 @@ + + + WorldListPage + + + + 0 + 0 + 800 + 600 + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + true + + + QAbstractItemView::DragDrop + + + true + + + false + + + false + + + true + + + true + + + false + + + + + + + + Actions + + + Qt::LeftToolBarArea|Qt::RightToolBarArea + + + Qt::ToolButtonTextOnly + + + false + + + RightToolBarArea + + + false + + + + + + + + + + + + + + + + + + + Add + + + + + Join + + + Launch the instance directly into the selected world + + + + + Join offline + + + Launch the instance in offline mode directly into the selected world + + + + + Rename + + + + + Copy + + + + + Remove + + + + + MCEdit + + + + + Copy Seed + + + + + Refresh + + + + + View Folder + + + + + Reset Icon + + + Remove world icon to make the game re-generate it on next load. + + + + + Datapacks + + + Manage datapacks inside the world. + + + + + + WideBar + QToolBar +
ui/widgets/WideBar.h
+
+
+ + +
diff --git a/ultimmc/launcher/ui/pages/modplatform/ImportPage.cpp b/ultimmc/launcher/ui/pages/modplatform/ImportPage.cpp new file mode 100644 index 0000000..c8e8e00 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/ImportPage.cpp @@ -0,0 +1,133 @@ +#include "ImportPage.h" +#include "ui_ImportPage.h" + +#include +#include + +#include "ui/dialogs/NewInstanceDialog.h" + +#include "InstanceImportTask.h" + + +class UrlValidator : public QValidator +{ +public: + using QValidator::QValidator; + + State validate(QString &in, int &pos) const + { + const QUrl url(in); + if (url.isValid() && !url.isRelative() && !url.isEmpty()) + { + return Acceptable; + } + else if (QFile::exists(in)) + { + return Acceptable; + } + else + { + return Intermediate; + } + } +}; + +ImportPage::ImportPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::ImportPage), dialog(dialog) +{ + ui->setupUi(this); + ui->modpackEdit->setValidator(new UrlValidator(ui->modpackEdit)); + connect(ui->modpackEdit, &QLineEdit::textChanged, this, &ImportPage::updateState); +} + +ImportPage::~ImportPage() +{ + delete ui; +} + +bool ImportPage::shouldDisplay() const +{ + return true; +} + +void ImportPage::openedImpl() +{ + updateState(); +} + +void ImportPage::updateState() +{ + if(!isOpened) + { + return; + } + if(ui->modpackEdit->hasAcceptableInput()) + { + QString input = ui->modpackEdit->text(); + auto url = QUrl::fromUserInput(input); + if(url.isLocalFile()) + { + // FIXME: actually do some validation of what's inside here... this is fake AF + QFileInfo fi(input); + // mrpack is a modrinth pack + if(fi.exists() && (fi.suffix() == "zip" || fi.suffix() == "mrpack")) + { + QFileInfo fi(url.fileName()); + dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); + dialog->setSuggestedIcon("default"); + } + } + else + { + if(input.endsWith("?client=y")) { + input.chop(9); + input.append("/file"); + url = QUrl::fromUserInput(input); + } + // hook, line and sinker. + QFileInfo fi(url.fileName()); + dialog->setSuggestedPack(fi.completeBaseName(), new InstanceImportTask(url)); + dialog->setSuggestedIcon("default"); + } + } + else + { + dialog->setSuggestedPack(); + } +} + +void ImportPage::setUrl(const QString& url) +{ + ui->modpackEdit->setText(url); + updateState(); +} + +void ImportPage::on_modpackBtn_clicked() +{ + const QUrl url = QFileDialog::getOpenFileUrl(this, tr("Choose modpack"), modpackUrl(), tr("Zip (*.zip *.mrpack)")); + if (url.isValid()) + { + if (url.isLocalFile()) + { + ui->modpackEdit->setText(url.toLocalFile()); + } + else + { + ui->modpackEdit->setText(url.toString()); + } + } +} + + +QUrl ImportPage::modpackUrl() const +{ + const QUrl url(ui->modpackEdit->text()); + if (url.isValid() && !url.isRelative() && !url.host().isEmpty()) + { + return url; + } + else + { + return QUrl::fromLocalFile(ui->modpackEdit->text()); + } +} diff --git a/ultimmc/launcher/ui/pages/modplatform/ImportPage.h b/ultimmc/launcher/ui/pages/modplatform/ImportPage.h new file mode 100644 index 0000000..aba4def --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/ImportPage.h @@ -0,0 +1,70 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "ui/pages/BasePage.h" +#include +#include "tasks/Task.h" + +namespace Ui +{ +class ImportPage; +} + +class NewInstanceDialog; + +class ImportPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit ImportPage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~ImportPage(); + virtual QString displayName() const override + { + return tr("Import from zip"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("viewfolder"); + } + virtual QString id() const override + { + return "import"; + } + virtual QString helpPage() const override + { + return "Zip-import"; + } + virtual bool shouldDisplay() const override; + + void setUrl(const QString & url); + void openedImpl() override; + +private slots: + void on_modpackBtn_clicked(); + void updateState(); + +private: + QUrl modpackUrl() const; + +private: + Ui::ImportPage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; +}; + diff --git a/ultimmc/launcher/ui/pages/modplatform/ImportPage.ui b/ultimmc/launcher/ui/pages/modplatform/ImportPage.ui new file mode 100644 index 0000000..eb63cbe --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/ImportPage.ui @@ -0,0 +1,52 @@ + + + ImportPage + + + + 0 + 0 + 546 + 405 + + + + + + + Browse + + + + + + + http:// + + + + + + + Local file or link to a direct download: + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/ultimmc/launcher/ui/pages/modplatform/VanillaPage.cpp b/ultimmc/launcher/ui/pages/modplatform/VanillaPage.cpp new file mode 100644 index 0000000..5c58c1f --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/VanillaPage.cpp @@ -0,0 +1,103 @@ +#include "VanillaPage.h" +#include "ui_VanillaPage.h" + +#include + +#include "Application.h" +#include "meta/Index.h" +#include "meta/VersionList.h" +#include "ui/dialogs/NewInstanceDialog.h" +#include "Filter.h" +#include "InstanceCreationTask.h" + +VanillaPage::VanillaPage(NewInstanceDialog *dialog, QWidget *parent) + : QWidget(parent), dialog(dialog), ui(new Ui::VanillaPage) +{ + ui->setupUi(this); + ui->tabWidget->tabBar()->hide(); + connect(ui->versionList, &VersionSelectWidget::selectedVersionChanged, this, &VanillaPage::setSelectedVersion); + filterChanged(); + connect(ui->alphaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->betaFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->snapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->oldSnapshotFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->releaseFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->experimentsFilter, &QCheckBox::stateChanged, this, &VanillaPage::filterChanged); + connect(ui->refreshBtn, &QPushButton::clicked, this, &VanillaPage::refresh); +} + +void VanillaPage::openedImpl() +{ + if(!initialized) + { + auto vlist = APPLICATION->metadataIndex()->get("net.minecraft"); + ui->versionList->initialize(vlist.get()); + initialized = true; + } + else + { + suggestCurrent(); + } +} + +void VanillaPage::refresh() +{ + ui->versionList->loadList(); +} + +void VanillaPage::filterChanged() +{ + QStringList out; + if(ui->alphaFilter->isChecked()) + out << "(old_alpha)"; + if(ui->betaFilter->isChecked()) + out << "(old_beta)"; + if(ui->snapshotFilter->isChecked()) + out << "(snapshot)"; + if(ui->oldSnapshotFilter->isChecked()) + out << "(old_snapshot)"; + if(ui->releaseFilter->isChecked()) + out << "(release)"; + if(ui->experimentsFilter->isChecked()) + out << "(experiment)"; + auto regexp = out.join('|'); + ui->versionList->setFilter(BaseVersionList::TypeRole, new RegexpFilter(regexp, false)); +} + +VanillaPage::~VanillaPage() +{ + delete ui; +} + +bool VanillaPage::shouldDisplay() const +{ + return true; +} + +BaseVersionPtr VanillaPage::selectedVersion() const +{ + return m_selectedVersion; +} + +void VanillaPage::suggestCurrent() +{ + if (!isOpened) + { + return; + } + + if(!m_selectedVersion) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(m_selectedVersion->descriptor(), new InstanceCreationTask(m_selectedVersion)); + dialog->setSuggestedIcon("default"); +} + +void VanillaPage::setSelectedVersion(BaseVersionPtr version) +{ + m_selectedVersion = version; + suggestCurrent(); +} diff --git a/ultimmc/launcher/ui/pages/modplatform/VanillaPage.h b/ultimmc/launcher/ui/pages/modplatform/VanillaPage.h new file mode 100644 index 0000000..fd4c2da --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/VanillaPage.h @@ -0,0 +1,75 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "ui/pages/BasePage.h" +#include +#include "tasks/Task.h" + +namespace Ui +{ +class VanillaPage; +} + +class NewInstanceDialog; + +class VanillaPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit VanillaPage(NewInstanceDialog *dialog, QWidget *parent = 0); + virtual ~VanillaPage(); + virtual QString displayName() const override + { + return tr("Vanilla"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("minecraft"); + } + virtual QString id() const override + { + return "vanilla"; + } + virtual QString helpPage() const override + { + return "Vanilla-platform"; + } + virtual bool shouldDisplay() const override; + void openedImpl() override; + + BaseVersionPtr selectedVersion() const; + +public slots: + void setSelectedVersion(BaseVersionPtr version); + +private slots: + void filterChanged(); + +private: + void refresh(); + void suggestCurrent(); + +private: + bool initialized = false; + NewInstanceDialog *dialog = nullptr; + Ui::VanillaPage *ui = nullptr; + bool m_versionSetByUser = false; + BaseVersionPtr m_selectedVersion; +}; diff --git a/ultimmc/launcher/ui/pages/modplatform/VanillaPage.ui b/ultimmc/launcher/ui/pages/modplatform/VanillaPage.ui new file mode 100644 index 0000000..870ff16 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/VanillaPage.ui @@ -0,0 +1,169 @@ + + + VanillaPage + + + + 0 + 0 + 815 + 607 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + + + + + + + + Filter + + + Qt::AlignCenter + + + + + + + Releases + + + true + + + true + + + + + + + Snapshots + + + true + + + + + + + Old Snapshots + + + true + + + + + + + Betas + + + true + + + + + + + Alphas + + + true + + + + + + + Experiments + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Refresh + + + + + + + + + + 0 + 0 + + + + + + + + + + + + + VersionSelectWidget + QWidget +
ui/widgets/VersionSelectWidget.h
+ 1 +
+
+ + tabWidget + releaseFilter + snapshotFilter + oldSnapshotFilter + betaFilter + alphaFilter + experimentsFilter + refreshBtn + + + +
diff --git a/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp new file mode 100644 index 0000000..3a97d47 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.cpp @@ -0,0 +1,97 @@ +/* + * Copyright 2020-2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AtlFilterModel.h" + +#include + +#include +#include +#include + +namespace Atl { + +FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + currentSorting = Sorting::ByPopularity; + sortings.insert(tr("Sort by popularity"), Sorting::ByPopularity); + sortings.insert(tr("Sort by name"), Sorting::ByName); + sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion); + + searchTerm = ""; +} + +const QMap FilterModel::getAvailableSortings() +{ + return sortings; +} + +QString FilterModel::translateCurrentSorting() +{ + return sortings.key(currentSorting); +} + +void FilterModel::setSorting(Sorting sorting) +{ + currentSorting = sorting; + invalidate(); +} + +FilterModel::Sorting FilterModel::getCurrentSorting() +{ + return currentSorting; +} + +void FilterModel::setSearchTerm(const QString term) +{ + searchTerm = term.trimmed(); + invalidate(); +} + +bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + if (searchTerm.isEmpty()) { + return true; + } + + QModelIndex index = sourceModel()->index(sourceRow, 0, sourceParent); + ATLauncher::IndexedPack pack = sourceModel()->data(index, Qt::UserRole).value(); + return pack.name.contains(searchTerm, Qt::CaseInsensitive); +} + +bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + ATLauncher::IndexedPack leftPack = sourceModel()->data(left, Qt::UserRole).value(); + ATLauncher::IndexedPack rightPack = sourceModel()->data(right, Qt::UserRole).value(); + + if (currentSorting == ByPopularity) { + return leftPack.position > rightPack.position; + } + else if (currentSorting == ByGameVersion) { + Version lv(leftPack.versions.at(0).minecraft); + Version rv(rightPack.versions.at(0).minecraft); + return lv < rv; + } + else if (currentSorting == ByName) { + return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; + } + + // Invalid sorting set, somehow... + qWarning() << "Invalid sorting set!"; + return true; +} + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h new file mode 100644 index 0000000..5235ccd --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlFilterModel.h @@ -0,0 +1,50 @@ +/* + * Copyright 2020-2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace Atl { + +class FilterModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + FilterModel(QObject* parent = Q_NULLPTR); + enum Sorting { + ByPopularity, + ByGameVersion, + ByName, + }; + const QMap getAvailableSortings(); + QString translateCurrentSorting(); + void setSorting(Sorting sorting); + Sorting getCurrentSorting(); + void setSearchTerm(QString term); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +private: + QMap sortings; + Sorting currentSorting; + QString searchTerm; + +}; + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp new file mode 100644 index 0000000..ef9a926 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlListModel.cpp @@ -0,0 +1,209 @@ +/* + * Copyright 2020-2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AtlListModel.h" + +#include +#include +#include + +namespace Atl { + +ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +ListModel::~ListModel() +{ +} + +int ListModel::rowCount(const QModelIndex &parent) const +{ + return modpacks.size(); +} + +int ListModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ListModel::data(const QModelIndex &index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + ATLauncher::IndexedPack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if (role == Qt::ToolTipRole) + { + return pack.name; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.safeName)) + { + return (m_logoMap.value(pack.safeName)); + } + auto icon = APPLICATION->getThemedIcon("atlauncher-placeholder"); + + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(pack.safeName.toLower()); + ((ListModel *)this)->requestLogo(pack.safeName, url); + + return icon; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +void ListModel::request() +{ + beginResetModel(); + modpacks.clear(); + endResetModel(); + + auto *netJob = new NetJob("Atl::Request", APPLICATION->network()); + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/json/packsnew.json"); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); + jobPtr = netJob; + jobPtr->start(); + + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::requestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::requestFailed); +} + +void ListModel::requestFinished() +{ + jobPtr.reset(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from ATL at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + QList newList; + + auto packs = doc.array(); + for(auto packRaw : packs) { + auto packObj = packRaw.toObject(); + + ATLauncher::IndexedPack pack; + + try { + ATLauncher::loadIndexedPack(pack, packObj); + } + catch (const JSONValidationError &e) { + qDebug() << QString::fromUtf8(response); + qWarning() << "Error while reading pack manifest from ATLauncher: " << e.cause(); + return; + } + + // ignore packs without a published version + if(pack.versions.length() == 0) continue; + // only display public packs (for now) + if(pack.type != ATLauncher::PackType::Public) continue; + // ignore "system" packs (Vanilla, Vanilla with Forge, etc) + if(pack.system) continue; + + newList.append(pack); + } + + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void ListModel::requestFailed(QString reason) +{ + jobPtr.reset(); +} + +void ListModel::getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +void ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void ListModel::logoLoaded(QString logo, QIcon out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, out); + + for(int i = 0; i < modpacks.size(); i++) { + if(modpacks[i].safeName == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + } + } +} + +void ListModel::requestLogo(QString file, QString url) +{ + if(m_loadingLogos.contains(file) || m_failedLogos.contains(file)) + { + return; + } + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ATLauncherPacks", QString("logos/%1").arg(file.section(".", 0, 0))); + NetJob *job = new NetJob(QString("ATLauncher Icon Download %1").arg(file), APPLICATION->network()); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::succeeded, this, [this, file, fullPath] + { + emit logoLoaded(file, QIcon(fullPath)); + if(waitingCallbacks.contains(file)) + { + waitingCallbacks.value(file)(fullPath); + } + }); + + QObject::connect(job, &NetJob::failed, this, [this, file] + { + emit logoFailed(file); + }); + + job->start(); + + m_loadingLogos.append(file); +} + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h new file mode 100644 index 0000000..2574c48 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlListModel.h @@ -0,0 +1,68 @@ +/* + * Copyright 2020-2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "net/NetJob.h" +#include +#include + +namespace Atl { + +typedef QMap LogoMap; +typedef std::function LogoCallback; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ListModel(QObject *parent); + virtual ~ListModel(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + + void request(); + + void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + +private slots: + void requestFinished(); + void requestFailed(QString reason); + + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + +private: + void requestLogo(QString file, QString url); + +private: + QList modpacks; + + QStringList m_failedLogos; + QStringList m_loadingLogos; + LogoMap m_logoMap; + QMap waitingCallbacks; + + NetJob::Ptr jobPtr; + QByteArray response; +}; + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp new file mode 100644 index 0000000..c0aaf45 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.cpp @@ -0,0 +1,246 @@ +/* + * Copyright 2021-2022 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AtlOptionalModDialog.h" +#include "ui_AtlOptionalModDialog.h" + +#include + +AtlOptionalModListModel::AtlOptionalModListModel(QWidget *parent, ATLauncher::PackVersion version, QVector mods) + : QAbstractListModel(parent), m_version(version), m_mods(mods) { + + // fill mod index + for (int i = 0; i < m_mods.size(); i++) { + auto mod = m_mods.at(i); + m_index[mod.name] = i; + } + // set initial state + for (int i = 0; i < m_mods.size(); i++) { + auto mod = m_mods.at(i); + m_selection[mod.name] = false; + setMod(mod, i, mod.selected, false); + } +} + +QVector AtlOptionalModListModel::getResult() { + QVector result; + + for (const auto& mod : m_mods) { + if (m_selection[mod.name]) { + result.push_back(mod.name); + } + } + + return result; +} + +int AtlOptionalModListModel::rowCount(const QModelIndex &parent) const { + return m_mods.size(); +} + +int AtlOptionalModListModel::columnCount(const QModelIndex &parent) const { + // Enabled, Name, Description + return 3; +} + +QVariant AtlOptionalModListModel::data(const QModelIndex &index, int role) const { + auto row = index.row(); + auto mod = m_mods.at(row); + + if (role == Qt::DisplayRole) { + if (index.column() == NameColumn) { + return mod.name; + } + if (index.column() == DescriptionColumn) { + return mod.description; + } + } + else if (role == Qt::ToolTipRole) { + if (index.column() == DescriptionColumn) { + return mod.description; + } + } + else if (role == Qt::ForegroundRole) { + if (!mod.colour.isEmpty() && m_version.colours.contains(mod.colour)) { + return QColor(QString("#%1").arg(m_version.colours[mod.colour])); + } + } + else if (role == Qt::CheckStateRole) { + if (index.column() == EnabledColumn) { + return m_selection[mod.name] ? Qt::Checked : Qt::Unchecked; + } + } + + return QVariant(); +} + +bool AtlOptionalModListModel::setData(const QModelIndex &index, const QVariant &value, int role) { + if (role == Qt::CheckStateRole) { + auto row = index.row(); + auto mod = m_mods.at(row); + + toggleMod(mod, row); + return true; + } + + return false; +} + +QVariant AtlOptionalModListModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { + switch (section) { + case EnabledColumn: + return QString(); + case NameColumn: + return QString("Name"); + case DescriptionColumn: + return QString("Description"); + } + } + + return QVariant(); +} + +Qt::ItemFlags AtlOptionalModListModel::flags(const QModelIndex &index) const { + auto flags = QAbstractListModel::flags(index); + if (index.isValid() && index.column() == EnabledColumn) { + flags |= Qt::ItemIsUserCheckable; + } + return flags; +} + +void AtlOptionalModListModel::selectRecommended() { + for (const auto& mod : m_mods) { + m_selection[mod.name] = mod.recommended; + } + + emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), + AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); +} + +void AtlOptionalModListModel::clearAll() { + for (const auto& mod : m_mods) { + m_selection[mod.name] = false; + } + + emit dataChanged(AtlOptionalModListModel::index(0, EnabledColumn), + AtlOptionalModListModel::index(m_mods.size() - 1, EnabledColumn)); +} + +void AtlOptionalModListModel::toggleMod(ATLauncher::VersionMod mod, int index) { + auto enable = !m_selection[mod.name]; + + // If there is a warning for the mod, display that first (if we would be enabling the mod) + if (enable && !mod.warning.isEmpty() && m_version.warnings.contains(mod.warning)) { + auto message = QString("%1

%2") + .arg(m_version.warnings[mod.warning], tr("Are you sure that you want to enable this mod?")); + + // fixme: avoid casting here + auto result = QMessageBox::warning((QWidget*) this->parent(), tr("Warning"), message, QMessageBox::Yes | QMessageBox::No); + if (result != QMessageBox::Yes) { + return; + } + } + + setMod(mod, index, enable); +} + +void AtlOptionalModListModel::setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit) { + if (m_selection[mod.name] == enable) return; + + m_selection[mod.name] = enable; + + // disable other mods in the group, if applicable + if (enable && !mod.group.isEmpty()) { + for (int i = 0; i < m_mods.size(); i++) { + if (index == i) continue; + auto other = m_mods.at(i); + + if (mod.group == other.group) { + setMod(other, i, false, shouldEmit); + } + } + } + + for (const auto& dependencyName : mod.depends) { + auto dependencyIndex = m_index[dependencyName]; + auto dependencyMod = m_mods.at(dependencyIndex); + + // enable/disable dependencies + if (enable) { + setMod(dependencyMod, dependencyIndex, true, shouldEmit); + } + + // if the dependency is 'effectively hidden', then track which mods + // depend on it - so we can efficiently disable it when no more dependents + // depend on it. + auto dependants = m_dependants[dependencyName]; + + if (enable) { + dependants.append(mod.name); + } + else { + dependants.removeAll(mod.name); + + // if there are no longer any dependents, let's disable the mod + if (dependencyMod.effectively_hidden && dependants.isEmpty()) { + setMod(dependencyMod, dependencyIndex, false, shouldEmit); + } + } + } + + // disable mods that depend on this one, if disabling + if (!enable) { + auto dependants = m_dependants[mod.name]; + for (const auto& dependencyName : dependants) { + auto dependencyIndex = m_index[dependencyName]; + auto dependencyMod = m_mods.at(dependencyIndex); + + setMod(dependencyMod, dependencyIndex, false, shouldEmit); + } + } + + if (shouldEmit) { + emit dataChanged(AtlOptionalModListModel::index(index, EnabledColumn), + AtlOptionalModListModel::index(index, EnabledColumn)); + } +} + + +AtlOptionalModDialog::AtlOptionalModDialog(QWidget *parent, ATLauncher::PackVersion version, QVector mods) + : QDialog(parent), ui(new Ui::AtlOptionalModDialog) { + ui->setupUi(this); + + listModel = new AtlOptionalModListModel(this, version, mods); + ui->treeView->setModel(listModel); + + ui->treeView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + ui->treeView->header()->setSectionResizeMode( + AtlOptionalModListModel::NameColumn, QHeaderView::ResizeToContents); + ui->treeView->header()->setSectionResizeMode( + AtlOptionalModListModel::DescriptionColumn, QHeaderView::Stretch); + + connect(ui->selectRecommendedButton, &QPushButton::pressed, + listModel, &AtlOptionalModListModel::selectRecommended); + connect(ui->clearAllButton, &QPushButton::pressed, + listModel, &AtlOptionalModListModel::clearAll); + connect(ui->installButton, &QPushButton::pressed, + this, &QDialog::close); +} + +AtlOptionalModDialog::~AtlOptionalModDialog() { + delete ui; +} diff --git a/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h new file mode 100644 index 0000000..26a2fdf --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.h @@ -0,0 +1,84 @@ +/* + * Copyright 2021-2022 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "modplatform/atlauncher/ATLPackIndex.h" + +namespace Ui { +class AtlOptionalModDialog; +} + +class AtlOptionalModListModel : public QAbstractListModel { + Q_OBJECT + +public: + enum Columns + { + EnabledColumn = 0, + NameColumn, + DescriptionColumn, + }; + + AtlOptionalModListModel(QWidget *parent, ATLauncher::PackVersion version, QVector mods); + + QVector getResult(); + + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + + QVariant data(const QModelIndex &index, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + + Qt::ItemFlags flags(const QModelIndex &index) const override; + +public slots: + void selectRecommended(); + void clearAll(); + +private: + void toggleMod(ATLauncher::VersionMod mod, int index); + void setMod(ATLauncher::VersionMod mod, int index, bool enable, bool shouldEmit = true); + +private: + ATLauncher::PackVersion m_version; + QVector m_mods; + + QMap m_selection; + QMap m_index; + QMap> m_dependants; +}; + +class AtlOptionalModDialog : public QDialog { + Q_OBJECT + +public: + AtlOptionalModDialog(QWidget *parent, ATLauncher::PackVersion version, QVector mods); + ~AtlOptionalModDialog() override; + + QVector getResult() { + return listModel->getResult(); + } + +private: + Ui::AtlOptionalModDialog *ui; + + AtlOptionalModListModel *listModel; +}; diff --git a/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui new file mode 100644 index 0000000..4c5c2ec --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlOptionalModDialog.ui @@ -0,0 +1,65 @@ + + + AtlOptionalModDialog + + + + 0 + 0 + 550 + 310 + + + + Select Mods To Install + + + + + + Install + + + true + + + + + + + Select Recommended + + + + + + + false + + + Use Share Code + + + + + + + Clear All + + + + + + + + + + + ModListView + QTreeView +
ui/widgets/ModListView.h
+
+
+ + +
diff --git a/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp new file mode 100644 index 0000000..98b0687 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlPage.cpp @@ -0,0 +1,195 @@ +/* + * Copyright 2020-2022 Jamie Mansfield + * Copyright 2021 Philip T + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AtlPage.h" +#include "ui_AtlPage.h" + +#include "modplatform/atlauncher/ATLPackInstallTask.h" + +#include "AtlOptionalModDialog.h" +#include "ui/dialogs/NewInstanceDialog.h" +#include "ui/dialogs/VersionSelectDialog.h" + +#include + +#include + +AtlPage::AtlPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::AtlPage), dialog(dialog) +{ + ui->setupUi(this); + + filterModel = new Atl::FilterModel(this); + listModel = new Atl::ListModel(this); + filterModel->setSourceModel(listModel); + ui->packView->setModel(filterModel); + ui->packView->setSortingEnabled(true); + + ui->packView->header()->hide(); + ui->packView->setIndentation(0); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + for(int i = 0; i < filterModel->getAvailableSortings().size(); i++) + { + ui->sortByBox->addItem(filterModel->getAvailableSortings().keys().at(i)); + } + ui->sortByBox->setCurrentText(filterModel->translateCurrentSorting()); + + connect(ui->searchEdit, &QLineEdit::textChanged, this, &AtlPage::triggerSearch); + connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &AtlPage::onSortingSelectionChanged); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &AtlPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &AtlPage::onVersionSelectionChanged); +} + +AtlPage::~AtlPage() +{ + delete ui; +} + +bool AtlPage::shouldDisplay() const +{ + return true; +} + +void AtlPage::openedImpl() +{ + if(!initialized) + { + listModel->request(); + initialized = true; + } + + suggestCurrent(); +} + +void AtlPage::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if (selectedVersion.isEmpty()) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(selected.name + " " + selectedVersion, new ATLauncher::PackInstallTask(this, selected.name, selectedVersion)); + auto editedLogoName = selected.safeName; + auto url = QString(BuildConfig.ATL_DOWNLOAD_SERVER_URL + "launcher/images/%1.png").arg(selected.safeName.toLower()); + listModel->getLogo(selected.safeName, url, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); +} + +void AtlPage::triggerSearch() +{ + filterModel->setSearchTerm(ui->searchEdit->text()); +} + +void AtlPage::onSortingSelectionChanged(QString data) +{ + auto toSet = filterModel->getAvailableSortings().value(data); + filterModel->setSorting(toSet); +} + +void AtlPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + + selected = filterModel->data(first, Qt::UserRole).value(); + + ui->packDescription->setHtml(selected.description.replace("\n", "
")); + + for(const auto& version : selected.versions) { + ui->versionSelectionBox->addItem(version.version); + } + + suggestCurrent(); +} + +void AtlPage::onVersionSelectionChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = ""; + return; + } + + selectedVersion = data; + suggestCurrent(); +} + +QVector AtlPage::chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) { + AtlOptionalModDialog optionalModDialog(this, version, mods); + optionalModDialog.exec(); + return optionalModDialog.getResult(); +} + +QString AtlPage::chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) { + VersionSelectDialog vselect(vlist.get(), "Choose Version", APPLICATION->activeWindow(), false); + if (minecraftVersion != Q_NULLPTR) { + vselect.setExactFilter(BaseVersionList::ParentVersionRole, minecraftVersion); + vselect.setEmptyString(tr("No versions are currently available for Minecraft %1").arg(minecraftVersion)); + } + else { + vselect.setEmptyString(tr("No versions are currently available")); + } + vselect.setEmptyErrorString(tr("Couldn't load or download the version lists!")); + + // select recommended build + for (int i = 0; i < vlist->versions().size(); i++) { + auto version = vlist->versions().at(i); + auto reqs = version->depends(); + + // filter by minecraft version, if the loader depends on a certain version. + if (minecraftVersion != Q_NULLPTR) { + auto iter = std::find_if(reqs.begin(), reqs.end(), [](const Meta::Require &req) { + return req.uid == "net.minecraft"; + }); + if (iter == reqs.end()) continue; + if (iter->equalsVersion != minecraftVersion) continue; + } + + // first recommended build we find, we use. + if (version->isRecommended()) { + vselect.setCurrentVersion(version->descriptor()); + break; + } + } + + vselect.exec(); + return vselect.selectedVersion()->descriptor(); +} + +void AtlPage::displayMessage(QString message) +{ + QMessageBox::information(this, tr("Installing"), message); +} diff --git a/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlPage.h b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlPage.h new file mode 100644 index 0000000..a7c56eb --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlPage.h @@ -0,0 +1,88 @@ +/* + * Copyright 2020-2022 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "AtlFilterModel.h" +#include "AtlListModel.h" + +#include +#include + +#include "Application.h" +#include "ui/pages/BasePage.h" +#include "tasks/Task.h" + +namespace Ui +{ + class AtlPage; +} + +class NewInstanceDialog; + +class AtlPage : public QWidget, public BasePage, public ATLauncher::UserInteractionSupport +{ +Q_OBJECT + +public: + explicit AtlPage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~AtlPage(); + virtual QString displayName() const override + { + return tr("ATLauncher"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("atlauncher"); + } + virtual QString id() const override + { + return "atl"; + } + virtual QString helpPage() const override + { + return "ATL-platform"; + } + virtual bool shouldDisplay() const override; + + void openedImpl() override; + +private: + void suggestCurrent(); + + QString chooseVersion(Meta::VersionListPtr vlist, QString minecraftVersion) override; + QVector chooseOptionalMods(ATLauncher::PackVersion version, QVector mods) override; + void displayMessage(QString message) override; + +private slots: + void triggerSearch(); + + void onSortingSelectionChanged(QString data); + + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(QString data); + +private: + Ui::AtlPage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; + Atl::ListModel* listModel = nullptr; + Atl::FilterModel* filterModel = nullptr; + + ATLauncher::IndexedPack selected; + QString selectedVersion; + + bool initialized = false; +}; diff --git a/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui new file mode 100644 index 0000000..9085766 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/atlauncher/AtlPage.ui @@ -0,0 +1,92 @@ + + + AtlPage + + + + 0 + 0 + 837 + 685 + + + + + + + + + true + + + + 96 + 48 + + + + + + + + true + + + true + + + + + + + Warning: This is still a work in progress. If you run into issues with the imported modpack, it may be a bug. + + + true + + + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + Search and filter ... + + + true + + + + + + + searchEdit + packView + packDescription + sortByBox + versionSelectionBox + + + + diff --git a/ultimmc/launcher/ui/pages/modplatform/import_ftb/FTBAPage.cpp b/ultimmc/launcher/ui/pages/modplatform/import_ftb/FTBAPage.cpp new file mode 100644 index 0000000..8a39538 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/import_ftb/FTBAPage.cpp @@ -0,0 +1,122 @@ +/* + * Copyright 2022 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include "FTBAPage.h" +#include "ui_FTBAPage.h" + +#include "Application.h" + +#include "ui/dialogs/NewInstanceDialog.h" + +#include "PackInstallTask.h" +#include "Model.h" + +namespace ImportFTB { + +FTBAPage::FTBAPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), dialog(dialog), ui(new Ui::FTBAPage) +{ + ui->setupUi(this); + + packModel = new Model(this); + + ui->packList->setModel(packModel); + ui->packList->setSortingEnabled(true); + ui->packList->header()->hide(); + ui->packList->setIndentation(0); + ui->packList->setIconSize(QSize(42, 42)); + + connect(ui->packList->selectionModel(), &QItemSelectionModel::currentChanged, this, &FTBAPage::onPublicPackSelectionChanged); + + ui->packList->selectionModel()->reset(); + + currentList = ui->packList; + currentModpackInfo = ui->packDescription; + + currentList->selectionModel()->reset(); + QModelIndex idx = currentList->currentIndex(); + if(idx.isValid()) + { + auto pack = packModel->data(idx, Qt::UserRole).value(); + onPackSelectionChanged(&pack); + } + else + { + onPackSelectionChanged(); + } +} + +FTBAPage::~FTBAPage() +{ + delete ui; +} + +bool FTBAPage::shouldDisplay() const +{ + return true; +} + +void FTBAPage::openedImpl() +{ + if(!initialized) + { + packModel->reload(); + initialized = true; + } + suggestCurrent(); +} + +void FTBAPage::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if(!selected) + { + dialog->setSuggestedPack(); + return; + } + auto selectedPack = *selected; + + dialog->setSuggestedPack(selectedPack.name, new PackInstallTask(selectedPack)); + QString logoName = QString("ftb_%1").arg(selectedPack.id); + dialog->setSuggestedIconFromFile(selectedPack.iconPath, logoName); +} + +void FTBAPage::onPublicPackSelectionChanged(QModelIndex now, QModelIndex prev) +{ + if(!now.isValid()) + { + onPackSelectionChanged(); + return; + } + Modpack selectedPack = packModel->data(now, Qt::UserRole).value(); + onPackSelectionChanged(&selectedPack); +} + +void FTBAPage::onPackSelectionChanged(Modpack* pack) +{ + if(pack) + { + currentModpackInfo->setHtml("Pack by " + pack->authors.join(", ") + "" + "
Minecraft " + pack->mcVersion + "
" + "
" + pack->description); + selected = *pack; + } + else + { + currentModpackInfo->setHtml(""); + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + suggestCurrent(); +} + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/import_ftb/FTBAPage.h b/ultimmc/launcher/ui/pages/modplatform/import_ftb/FTBAPage.h new file mode 100644 index 0000000..e17788c --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/import_ftb/FTBAPage.h @@ -0,0 +1,79 @@ +/* + * Copyright 2022 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include +#include +#include + +#include "ui/pages/BasePage.h" +#include +#include "tasks/Task.h" +#include "QObjectPtr.h" + +#include "Model.h" + +class NewInstanceDialog; + +namespace ImportFTB { + +namespace Ui +{ +class FTBAPage; +} + +class Model; + +class FTBAPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit FTBAPage(NewInstanceDialog * dialog, QWidget *parent = 0); + virtual ~FTBAPage(); + QString displayName() const override + { + return tr("FTB App Import"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("ftb_logo"); + } + QString id() const override + { + return "import_ftb"; + } + QString helpPage() const override + { + return "FTB-app"; + } + bool shouldDisplay() const override; + void openedImpl() override; + +private: + void suggestCurrent(); + void onPackSelectionChanged(Modpack *pack = nullptr); + +private slots: + void onPublicPackSelectionChanged(QModelIndex first, QModelIndex second); + +private: + QTreeView* currentList = nullptr; + QTextBrowser* currentModpackInfo = nullptr; + + bool initialized = false; + nonstd::optional selected; + + Model* packModel = nullptr; + + NewInstanceDialog* dialog = nullptr; + + Ui::FTBAPage *ui = nullptr; +}; + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/import_ftb/FTBAPage.ui b/ultimmc/launcher/ui/pages/modplatform/import_ftb/FTBAPage.ui new file mode 100644 index 0000000..cc7aeaf --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/import_ftb/FTBAPage.ui @@ -0,0 +1,40 @@ + + + + ImportFTB::FTBAPage + + + + 0 + 0 + 1461 + 1011 + + + + + + + + 16777215 + 16777215 + + + + true + + + + + + + + + + + diff --git a/ultimmc/launcher/ui/pages/modplatform/import_ftb/Model.cpp b/ultimmc/launcher/ui/pages/modplatform/import_ftb/Model.cpp new file mode 100644 index 0000000..95a5c44 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/import_ftb/Model.cpp @@ -0,0 +1,351 @@ +/* + * Copyright 2022 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include "Model.h" +#include "Application.h" + +#include +#include + +#include +#include + +#include + +#include +#include +#include +#include + +#if defined(Q_OS_WIN32) +#include +constexpr int BUFFER_SIZE = 1024; +#endif + +namespace ImportFTB { + +Model::Model(QObject *parent) : QAbstractListModel(parent) +{ +} + +int Model::rowCount(const QModelIndex &parent) const +{ + return modpacks.size(); +} + +int Model::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant Model::data(const QModelIndex &index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QVariant(); + } + + Modpack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if(role == Qt::DecorationRole) + { + return pack.icon; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +namespace { + +#if defined (Q_OS_OSX) +QString getFTBAPath() { + return FS::PathCombine(QDir::homePath(), "Library/Application Support/.ftba"); +} +#elif defined(Q_OS_WIN32) +QString getFTBAPath() { + wchar_t buf[BUFFER_SIZE]; + if(!GetEnvironmentVariableW(L"LOCALAPPDATA", buf, BUFFER_SIZE)) + { + return QString(); + } + QString appDataLocal = QString::fromWCharArray(buf); + QString settingsPath = FS::PathCombine(appDataLocal, ".ftba"); + return settingsPath; +} +#else +QString getFTBAPath() { + return FS::PathCombine(QDir::homePath(), ".ftba"); +} +#endif + +QString getFTBASettingsPath() { + QString returnpath = FS::PathCombine(getFTBAPath(), "storage/settings.json"); + if (QFileInfo::exists(returnpath)) + return returnpath; + return FS::PathCombine(getFTBAPath(), "bin/settings.json"); +} + +QString getFTBAVersionPath(const QString& id) { + return FS::PathCombine(getFTBAPath(), QString("bin/versions/%1/%1.json").arg(id)); +} + +QString getFTBAInstances() { + QByteArray data; + auto settingsPath = getFTBASettingsPath(); + try + { + data = FS::read(settingsPath); + } + catch (const Exception &e) + { + qWarning() << "Could not read FTB App settings file: " << settingsPath; + return QString(); + } + + try + { + auto document = Json::requireDocument(data); + auto object = Json::requireObject(document); + return Json::requireString(object, "instanceLocation"); + } + catch (Json::JsonException & e) + { + qCritical() << "Could not read FTB App settings file as JSON: " << e.cause(); + return QString(); + } +} + +/* +Reference from an FTB App file, as of 28.05.2022 +{ + "_private": false, + "uuid": "25850fc6-ec61-4bc6-8397-77e4497bf922", + "id": 95, + "art": "data:image/png;base64,...==", + "path": "/home/peterix/.ftba/instances/25850fc6-ec61-4bc6-8397-77e4497bf922", + "versionId": 2127, + "name": "FTB Presents Direwolf20 1.18", + "minMemory": 4096, + "recMemory": 6144, + "memory": 6144, + "version": "1.2.0", + "dir": "/home/peterix/.ftba/instances/25850fc6-ec61-4bc6-8397-77e4497bf922", + "authors": [ + "FTB Team" + ], + "description": "Direwolf's modded legacy started all the way back in 2011. With 5 modpacks from the FTB Team, dedicated to creating a simple yet unique form of kitchen-sink modpacks.\n\nThis new iteration of Direwolf's modpack is packed with hand-selected mods from Direwolf20 himself, the FTB Team have worked hard in forging a kitchen-sink styled modded Minecraft experience that just about anyone can play. \n\nFollow along with Direwolf on his YouTube series, learning with him as you venture out into new modded territory. Improve his designs as you build up more infrastructure and automate anything and everything. The world is yours, for you to do anything in.", + "mcVersion": "1.18.2", + "jvmArgs": "", + "embeddedJre": true, + "url": "", + "artUrl": "https://apps.modpacks.ch/modpacks/art/90/1024_1024.png", + "width": 1920, + "height": 1080, + "modLoader": "1.18.2-forge-40.1.20", + "isModified": false, + "isImport": false, + "cloudSaves": false, + "hasInstMods": false, + "installComplete": true, + "packType": 0, + "totalPlayTime": 0, + "lastPlayed": 1653168242 +} +*/ + +void resolveModloader(QString mcVersion, ModLoader &loader) { + if(loader.id.isEmpty()) { + loader.type = ModLoaderType::None; + return; + } + auto path = getFTBAVersionPath(loader.id); + QByteArray data; + + try + { + data = FS::read(path); + } + catch (const Exception &e) + { + qWarning() << "Could not resolve modloader ID: " << loader.id << "File cannot be loaded: " << path; + loader.type = ModLoaderType::Unresolved; + return; + } + try + { + QJsonDocument doc = Json::requireDocument(data); + QJsonObject root = Json::requireObject(doc, "version.json"); + bool isProbablyNeoforge = false; + for (auto library: Json::ensureArray(root, "libraries", {})) + { + if (!library.isObject()) + { + continue; + } + + auto libraryObject = Json::ensureValueObject(library, {}, ""); + GradleSpecifier name = Json::requireString(libraryObject, "name"); + auto artifactPrefix = name.artifactPrefix(); + + if(artifactPrefix.startsWith("net.neoforged.fancymodloader:")) { + isProbablyNeoforge = true; + break; + } + if(artifactPrefix == "net.minecraftforge:forge") { + QString libraryVersion = name.version(); + loader.type = ModLoaderType::Forge; + // remove any garbage 'minecraft version' prefix / suffix + libraryVersion.remove(QString("%1-").arg(mcVersion)); + libraryVersion.remove(QString("-%1").arg(mcVersion)); + loader.version = libraryVersion; + return; + } + else if(artifactPrefix == "net.minecraftforge:minecraftforge") { + loader.type = ModLoaderType::Forge; + loader.version = name.version(); + return; + } + else if (artifactPrefix == "net.fabricmc:fabric-loader") + { + loader.type = ModLoaderType::Fabric; + loader.version = name.version(); + return; + } + else if (artifactPrefix == "org.quiltmc:quilt-loader") + { + loader.type = ModLoaderType::Quilt; + loader.version = name.version(); + return; + } + } + // Weird detection for 'modern' forge + QJsonObject arguments = Json::ensureObject(root, "arguments"); + auto gameArgs = Json::ensureArray(arguments, "game"); + bool versionIsNext = false; + for (auto arg: gameArgs) { + QString value = Json::ensureValueString(arg, QString()); + if(versionIsNext) { + if(isProbablyNeoforge) { + loader.type = ModLoaderType::NeoForge; + } + else { + loader.type = ModLoaderType::Forge; + } + loader.version = value; + return; + } + if(value == "--fml.forgeVersion") { + versionIsNext = true; + } + } + loader.type = ModLoaderType::Unresolved; + return; + } + catch (const JSONValidationError &e) + { + qWarning() << "Could not resolve modloader ID: " << loader.id << "File cannot be understood: " << path << "Error: " << e.cause(); + loader.type = ModLoaderType::Unresolved; + } +} + +bool parseModpackJson(const QByteArray& data, Modpack & out) { + try + { + auto document = Json::requireDocument(data); + auto object = Json::requireObject(document); + bool isInstalled = Json::ensureBoolean(object, "installComplete", true); + if(!isInstalled) { + return false; + } + out.id = Json::requireInteger(object, "id"); + out.name = Json::requireString(object, "name"); + out.version = Json::requireString(object, "version"); + out.description = Json::ensureString(object, "description", QObject::tr("Description is missing in the FTB App instance.")); + auto authorsArray = Json::ensureArray(object, "authors", QJsonArray()); + for(auto author: authorsArray) { + out.authors.append(Json::requireValueString(author)); + } + + out.mcVersion = Json::requireString(object, "mcVersion"); + out.modLoader.id = Json::ensureString(object, "modLoader", QString()); + resolveModloader(out.mcVersion, out.modLoader); + out.hasInstMods = Json::ensureBoolean(object, "hasInstMods", false); + + out.minMemory = Json::ensureInteger(object, "minMemory", 1024); + out.recMemory = Json::ensureInteger(object, "recMemory", 2048); + out.memory = Json::ensureInteger(object, "memory", 2048); + return true; + } + catch (Json::JsonException & e) + { + qCritical() << "Could not read FTB App settings file as JSON: " << e.cause(); + return false; + } +} + +bool tryLoadInstance(const QString& location, Modpack & out) { + QFileInfo dirInfo(location); + + if(dirInfo.isSymLink()) + return false; + + QByteArray data; + auto jsonLocation = FS::PathCombine(location, "instance.json"); + try + { + data = FS::read(jsonLocation); + if(!parseModpackJson(data, out)) { + return false; + } + out.instanceDir = location; + out.iconPath = FS::PathCombine(location, "folder.jpg"); + out.icon = QIcon(out.iconPath); + return true; + } + catch (const Exception &e) + { + qWarning() << "Could not read FTB App instance JSON file: " << jsonLocation; + return false; + } + return false; +} + +} + +void Model::reload() { + beginResetModel(); + modpacks.clear(); + QString instancesLocation = getFTBAInstances(); + if(instancesLocation.isEmpty()) { + qDebug() << "FTB instances are not found..."; + } + else { + qDebug() << "Discovering FTB instances in" << instancesLocation; + QDirIterator iter(instancesLocation, QDir::Dirs | QDir::NoDot | QDir::NoDotDot | QDir::Readable | QDir::Hidden, QDirIterator::FollowSymlinks); + while (iter.hasNext()) { + QString subDir = iter.next(); + ImportFTB::Modpack out; + if(!tryLoadInstance(subDir, out)) { + continue; + } + modpacks.append(out); + } + } + endResetModel(); +} + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/import_ftb/Model.h b/ultimmc/launcher/ui/pages/modplatform/import_ftb/Model.h new file mode 100644 index 0000000..37e1432 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/import_ftb/Model.h @@ -0,0 +1,83 @@ +/* + * Copyright 2022 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace ImportFTB { + +enum class ModLoaderType { + Unresolved, + None, + Forge, + NeoForge, + Fabric, + Quilt +}; + +struct ModLoader { + QString id; + ModLoaderType type = ModLoaderType::Unresolved; + QString version; +}; + +struct Modpack { + int id = 0; + int versionId = 0; + + QString name; + QString version; + QString description; + QStringList authors; + + QString mcVersion; + ModLoader modLoader; + bool hasInstMods = false; + + int minMemory = 1024; + int recMemory = 2048; + int memory = 2048; + + QString instanceDir; + QString iconPath; + QIcon icon; + + QString getLogoName() { + return QString("ftb_%1").arg(id); + } +}; + +class Model : public QAbstractListModel +{ + Q_OBJECT +private: + QList modpacks; + +public: + Model(QObject *parent); + virtual ~Model() = default; + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + + void reload(); +}; + +} + +Q_DECLARE_METATYPE(ImportFTB::Modpack) diff --git a/ultimmc/launcher/ui/pages/modplatform/import_ftb/PackInstallTask.cpp b/ultimmc/launcher/ui/pages/modplatform/import_ftb/PackInstallTask.cpp new file mode 100644 index 0000000..6c0c43b --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/import_ftb/PackInstallTask.cpp @@ -0,0 +1,111 @@ +/* + * Copyright 2022 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include "PackInstallTask.h" + +#include + +#include "MMCZip.h" +#include "BaseInstance.h" +#include "FileSystem.h" +#include "settings/INISettingsObject.h" +#include "minecraft/MinecraftInstance.h" +#include "minecraft/PackProfile.h" +#include "minecraft/GradleSpecifier.h" + +#include "BuildConfig.h" +#include "Application.h" + +#include + +namespace ImportFTB { + +PackInstallTask::PackInstallTask(Modpack pack) { + m_pack = pack; +} + +void PackInstallTask::executeTask() { + FS::copy folderCopy(m_pack.instanceDir, FS::PathCombine(m_stagingPath, ".minecraft")); + folderCopy.followSymlinks(true).blacklist(new RegexpMatcher("(instance|version)[.]json")); + + m_copyFuture = QtConcurrent::run(QThreadPool::globalInstance(), folderCopy); + connect(&m_copyFutureWatcher, &QFutureWatcher::finished, this, &PackInstallTask::copyFinished); + connect(&m_copyFutureWatcher, &QFutureWatcher::canceled, this, &PackInstallTask::copyAborted); + m_copyFutureWatcher.setFuture(m_copyFuture); + abortable = true; +} + +void PackInstallTask::copyFinished() { + abortable = false; + QString instanceConfigPath = FS::PathCombine(m_stagingPath, "instance.cfg"); + auto instanceSettings = std::make_shared(instanceConfigPath); + instanceSettings->suspendSave(); + instanceSettings->registerSetting("InstanceType", "Legacy"); + instanceSettings->set("InstanceType", "OneSix"); + + MinecraftInstance instance(m_globalSettings, instanceSettings, m_stagingPath); + auto components = instance.getPackProfile(); + components->buildingFromScratch(); + components->setComponentVersion("net.minecraft", m_pack.mcVersion, true); + + auto modloader = m_pack.modLoader; + switch(modloader.type) { + case ModLoaderType::None: { + // NOOP + break; + } + case ModLoaderType::Forge: { + components->setComponentVersion("net.minecraftforge", modloader.version, true); + break; + } + case ModLoaderType::NeoForge: { + components->setComponentVersion("net.neoforged", modloader.version, true); + break; + } + case ModLoaderType::Fabric: { + components->setComponentVersion("net.fabricmc.fabric-loader", modloader.version, true); + break; + } + case ModLoaderType::Quilt: { + components->setComponentVersion("org.quiltmc.quilt-loader", modloader.version, true); + break; + } + case ModLoaderType::Unresolved: { + qWarning() << "Unknown modloader version: " << modloader.id; + } + } + components->saveNow(); + + instance.setName(m_instName); + if(m_instIcon == "default") + { + m_instIcon = "ftb_logo"; + } + instance.setIconKey(m_instIcon); + instanceSettings->resumeSave(); + + emitSucceeded(); +} + +void PackInstallTask::copyAborted() { + emitAborted(); +} + + +bool ImportFTB::PackInstallTask::canAbort() const { + return abortable; +} + +bool ImportFTB::PackInstallTask::abort() { + if(abortable) { + m_copyFuture.cancel(); + return true; + } + return false; +} + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/import_ftb/PackInstallTask.h b/ultimmc/launcher/ui/pages/modplatform/import_ftb/PackInstallTask.h new file mode 100644 index 0000000..04d37a7 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/import_ftb/PackInstallTask.h @@ -0,0 +1,46 @@ +/* + * Copyright 2022 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include "InstanceTask.h" +#include "Model.h" + +#include +#include + +namespace ImportFTB { + +class PackInstallTask : public InstanceTask +{ + Q_OBJECT + +public: + explicit PackInstallTask(Modpack pack); + virtual ~PackInstallTask() = default; + + bool canAbort() const override; + bool abort() override; + +protected: + virtual void executeTask() override; + +private: + void install(); + +private slots: + void copyFinished(); + void copyAborted(); + +private: + bool abortable = false; + Modpack m_pack; + QFuture m_copyFuture; + QFutureWatcher m_copyFutureWatcher; +}; + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp b/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp new file mode 100644 index 0000000..9c46e88 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/ListModel.cpp @@ -0,0 +1,259 @@ +#include "ListModel.h" +#include "Application.h" + +#include +#include + +#include +#include + +#include + +#include + +namespace LegacyFTB { + +FilterModel::FilterModel(QObject *parent) : QSortFilterProxyModel(parent) +{ + currentSorting = Sorting::ByGameVersion; + sortings.insert(tr("Sort by name"), Sorting::ByName); + sortings.insert(tr("Sort by game version"), Sorting::ByGameVersion); +} + +bool FilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const +{ + Modpack leftPack = sourceModel()->data(left, Qt::UserRole).value(); + Modpack rightPack = sourceModel()->data(right, Qt::UserRole).value(); + + if(currentSorting == Sorting::ByGameVersion) { + Version lv(leftPack.mcVersion); + Version rv(rightPack.mcVersion); + return lv < rv; + + } else if(currentSorting == Sorting::ByName) { + return Strings::naturalCompare(leftPack.name, rightPack.name, Qt::CaseSensitive) >= 0; + } + + //UHM, some inavlid value set?! + qWarning() << "Invalid sorting set!"; + return true; +} + +bool FilterModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const +{ + return true; +} + +const QMap FilterModel::getAvailableSortings() +{ + return sortings; +} + +QString FilterModel::translateCurrentSorting() +{ + return sortings.key(currentSorting); +} + +void FilterModel::setSorting(Sorting s) +{ + currentSorting = s; + invalidate(); +} + +FilterModel::Sorting FilterModel::getCurrentSorting() +{ + return currentSorting; +} + +ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +ListModel::~ListModel() +{ +} + +QString ListModel::translatePackType(PackType type) const +{ + switch(type) + { + case PackType::Public: + return tr("Public Modpack"); + case PackType::ThirdParty: + return tr("Third Party Modpack"); + case PackType::Private: + return tr("Private Modpack"); + } + qWarning() << "Unknown FTB modpack type:" << int(type); + return QString(); +} + +int ListModel::rowCount(const QModelIndex &parent) const +{ + return modpacks.size(); +} + +int ListModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +QVariant ListModel::data(const QModelIndex &index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + Modpack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name + "\n" + translatePackType(pack.type); + } + else if (role == Qt::ToolTipRole) + { + if(pack.description.length() > 100) + { + //some magic to prevent to long tooltips and replace html linebreaks + QString edit = pack.description.left(97); + edit = edit.left(edit.lastIndexOf("
")).left(edit.lastIndexOf(" ")).append("..."); + return edit; + + } + return pack.description; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.logo)) + { + return (m_logoMap.value(pack.logo)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ListModel *)this)->requestLogo(pack.logo); + return icon; + } + else if(role == Qt::TextColorRole) + { + if(pack.broken) + { + //FIXME: Hardcoded color + return QColor(255, 0, 50); + } + else if(pack.bugged) + { + //FIXME: Hardcoded color + //bugged pack, currently only indicates bugged xml + return QColor(244, 229, 66); + } + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + + return QVariant(); +} + +void ListModel::fill(ModpackList modpacks) +{ + beginResetModel(); + this->modpacks = modpacks; + endResetModel(); +} + +void ListModel::addPack(Modpack modpack) +{ + beginResetModel(); + this->modpacks.append(modpack); + endResetModel(); +} + +void ListModel::clear() +{ + beginResetModel(); + modpacks.clear(); + endResetModel(); +} + +Modpack ListModel::at(int row) +{ + return modpacks.at(row); +} + +void ListModel::remove(int row) +{ + if(row < 0 || row >= modpacks.size()) + { + qWarning() << "Attempt to remove FTB modpacks with invalid row" << row; + return; + } + beginRemoveRows(QModelIndex(), row, row); + modpacks.removeAt(row); + endRemoveRows(); +} + +void ListModel::logoLoaded(QString logo, QIcon out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, out); + emit dataChanged(createIndex(0, 0), createIndex(1, 0)); +} + +void ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void ListModel::requestLogo(QString file) +{ + if(m_loadingLogos.contains(file) || m_failedLogos.contains(file)) + { + return; + } + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(file.section(".", 0, 0))); + NetJob *job = new NetJob(QString("FTB Icon Download for %1").arg(file), APPLICATION->network()); + job->addNetAction(Net::Download::makeCached(QUrl(QString(BuildConfig.LEGACY_FTB_CDN_BASE_URL + "static/%1").arg(file)), entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::finished, this, [this, file, fullPath] + { + emit logoLoaded(file, QIcon(fullPath)); + if(waitingCallbacks.contains(file)) + { + waitingCallbacks.value(file)(fullPath); + } + }); + + QObject::connect(job, &NetJob::failed, this, [this, file] + { + emit logoFailed(file); + }); + + job->start(); + + m_loadingLogos.append(file); +} + +void ListModel::getLogo(const QString &logo, LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(APPLICATION->metacache()->resolveEntry("FTBPacks", QString("logos/%1").arg(logo.section(".", 0, 0)))->getFullPath()); + } + else + { + requestLogo(logo); + } +} + +Qt::ItemFlags ListModel::flags(const QModelIndex &index) const +{ + return QAbstractListModel::flags(index); +} + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h b/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h new file mode 100644 index 0000000..c55df00 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/ListModel.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include + +#include + +namespace LegacyFTB { + +typedef QMap FTBLogoMap; +typedef std::function LogoCallback; + +class FilterModel : public QSortFilterProxyModel +{ + Q_OBJECT +public: + FilterModel(QObject* parent = Q_NULLPTR); + enum Sorting { + ByName, + ByGameVersion + }; + const QMap getAvailableSortings(); + QString translateCurrentSorting(); + void setSorting(Sorting sorting); + Sorting getCurrentSorting(); + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; + bool lessThan(const QModelIndex &left, const QModelIndex &right) const override; + +private: + QMap sortings; + Sorting currentSorting; + +}; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT +private: + ModpackList modpacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + FTBLogoMap m_logoMap; + QMap waitingCallbacks; + + void requestLogo(QString file); + QString translatePackType(PackType type) const; + + +private slots: + void logoFailed(QString logo); + void logoLoaded(QString logo, QIcon out); + +public: + ListModel(QObject *parent); + ~ListModel(); + int rowCount(const QModelIndex &parent) const override; + int columnCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + + void fill(ModpackList modpacks); + void addPack(Modpack modpack); + void clear(); + void remove(int row); + + Modpack at(int row); + void getLogo(const QString &logo, LogoCallback callback); +}; + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp b/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp new file mode 100644 index 0000000..891704d --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/Page.cpp @@ -0,0 +1,371 @@ +#include "Page.h" +#include "ui_Page.h" + +#include + +#include "Application.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/dialogs/NewInstanceDialog.h" + +#include "modplatform/legacy_ftb/PackFetchTask.h" +#include "modplatform/legacy_ftb/PackInstallTask.h" +#include "modplatform/legacy_ftb/PrivatePackManager.h" +#include "ListModel.h" + +namespace LegacyFTB { + +Page::Page(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), dialog(dialog), ui(new Ui::Page) +{ + ftbFetchTask.reset(new PackFetchTask(APPLICATION->network())); + ftbPrivatePacks.reset(new PrivatePackManager()); + + ui->setupUi(this); + + { + publicFilterModel = new FilterModel(this); + publicListModel = new ListModel(this); + publicFilterModel->setSourceModel(publicListModel); + + ui->publicPackList->setModel(publicFilterModel); + ui->publicPackList->setSortingEnabled(true); + ui->publicPackList->header()->hide(); + ui->publicPackList->setIndentation(0); + ui->publicPackList->setIconSize(QSize(42, 42)); + + for(int i = 0; i < publicFilterModel->getAvailableSortings().size(); i++) + { + ui->sortByBox->addItem(publicFilterModel->getAvailableSortings().keys().at(i)); + } + + ui->sortByBox->setCurrentText(publicFilterModel->translateCurrentSorting()); + } + + { + thirdPartyFilterModel = new FilterModel(this); + thirdPartyModel = new ListModel(this); + thirdPartyFilterModel->setSourceModel(thirdPartyModel); + + ui->thirdPartyPackList->setModel(thirdPartyFilterModel); + ui->thirdPartyPackList->setSortingEnabled(true); + ui->thirdPartyPackList->header()->hide(); + ui->thirdPartyPackList->setIndentation(0); + ui->thirdPartyPackList->setIconSize(QSize(42, 42)); + + thirdPartyFilterModel->setSorting(publicFilterModel->getCurrentSorting()); + } + + { + privateFilterModel = new FilterModel(this); + privateListModel = new ListModel(this); + privateFilterModel->setSourceModel(privateListModel); + + ui->privatePackList->setModel(privateFilterModel); + ui->privatePackList->setSortingEnabled(true); + ui->privatePackList->header()->hide(); + ui->privatePackList->setIndentation(0); + ui->privatePackList->setIconSize(QSize(42, 42)); + + privateFilterModel->setSorting(publicFilterModel->getCurrentSorting()); + } + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + connect(ui->sortByBox, &QComboBox::currentTextChanged, this, &Page::onSortingSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &Page::onVersionSelectionItemChanged); + + connect(ui->publicPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPublicPackSelectionChanged); + connect(ui->thirdPartyPackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onThirdPartyPackSelectionChanged); + connect(ui->privatePackList->selectionModel(), &QItemSelectionModel::currentChanged, this, &Page::onPrivatePackSelectionChanged); + + connect(ui->addPackBtn, &QPushButton::pressed, this, &Page::onAddPackClicked); + connect(ui->removePackBtn, &QPushButton::pressed, this, &Page::onRemovePackClicked); + + connect(ui->tabWidget, &QTabWidget::currentChanged, this, &Page::onTabChanged); + + // ui->modpackInfo->setOpenExternalLinks(true); + + ui->publicPackList->selectionModel()->reset(); + ui->thirdPartyPackList->selectionModel()->reset(); + ui->privatePackList->selectionModel()->reset(); + + onTabChanged(ui->tabWidget->currentIndex()); +} + +Page::~Page() +{ + delete ui; +} + +bool Page::shouldDisplay() const +{ + return true; +} + +void Page::openedImpl() +{ + if(!initialized) + { + connect(ftbFetchTask.get(), &PackFetchTask::finished, this, &Page::ftbPackDataDownloadSuccessfully); + connect(ftbFetchTask.get(), &PackFetchTask::failed, this, &Page::ftbPackDataDownloadFailed); + + connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFinished, this, &Page::ftbPrivatePackDataDownloadSuccessfully); + connect(ftbFetchTask.get(), &PackFetchTask::privateFileDownloadFailed, this, &Page::ftbPrivatePackDataDownloadFailed); + + ftbFetchTask->fetch(); + ftbPrivatePacks->load(); + ftbFetchTask->fetchPrivate(ftbPrivatePacks->getCurrentPackCodes().toList()); + initialized = true; + } + suggestCurrent(); +} + +void Page::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if(selected.broken || selectedVersion.isEmpty()) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(selected.name, new PackInstallTask(APPLICATION->network(), selected, selectedVersion)); + QString editedLogoName; + if(selected.logo.toLower().startsWith("ftb")) + { + editedLogoName = selected.logo; + } + else + { + editedLogoName = "ftb_" + selected.logo; + } + + editedLogoName = editedLogoName.left(editedLogoName.lastIndexOf(".png")); + + if(selected.type == PackType::Public) + { + publicListModel->getLogo(selected.logo, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + } + else if (selected.type == PackType::ThirdParty) + { + thirdPartyModel->getLogo(selected.logo, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + } + else if (selected.type == PackType::Private) + { + privateListModel->getLogo(selected.logo, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + } +} + +void Page::ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks) +{ + publicListModel->fill(publicPacks); + thirdPartyModel->fill(thirdPartyPacks); +} + +void Page::ftbPackDataDownloadFailed(QString reason) +{ + //TODO: Display the error +} + +void Page::ftbPrivatePackDataDownloadSuccessfully(Modpack pack) +{ + privateListModel->addPack(pack); +} + +void Page::ftbPrivatePackDataDownloadFailed(QString reason, QString packCode) +{ + auto reply = QMessageBox::question( + this, + tr("FTB private packs"), + tr("Failed to download pack information for code %1.\nShould it be removed now?").arg(packCode) + ); + if(reply == QMessageBox::Yes) + { + ftbPrivatePacks->remove(packCode); + } +} + +void Page::onPublicPackSelectionChanged(QModelIndex now, QModelIndex prev) +{ + if(!now.isValid()) + { + onPackSelectionChanged(); + return; + } + Modpack selectedPack = publicFilterModel->data(now, Qt::UserRole).value(); + onPackSelectionChanged(&selectedPack); +} + +void Page::onThirdPartyPackSelectionChanged(QModelIndex now, QModelIndex prev) +{ + if(!now.isValid()) + { + onPackSelectionChanged(); + return; + } + Modpack selectedPack = thirdPartyFilterModel->data(now, Qt::UserRole).value(); + onPackSelectionChanged(&selectedPack); +} + +void Page::onPrivatePackSelectionChanged(QModelIndex now, QModelIndex prev) +{ + if(!now.isValid()) + { + onPackSelectionChanged(); + return; + } + Modpack selectedPack = privateFilterModel->data(now, Qt::UserRole).value(); + onPackSelectionChanged(&selectedPack); +} + +void Page::onPackSelectionChanged(Modpack* pack) +{ + ui->versionSelectionBox->clear(); + if(pack) + { + currentModpackInfo->setHtml("Pack by " + pack->author + "" + + "
Minecraft " + pack->mcVersion + "
" + "
" + pack->description + "
  • " + pack->mods.replace(";", "
  • ") + + "
"); + bool currentAdded = false; + + for(int i = 0; i < pack->oldVersions.size(); i++) + { + if(pack->currentVersion == pack->oldVersions.at(i)) + { + currentAdded = true; + } + ui->versionSelectionBox->addItem(pack->oldVersions.at(i)); + } + + if(!currentAdded) + { + ui->versionSelectionBox->addItem(pack->currentVersion); + } + selected = *pack; + } + else + { + currentModpackInfo->setHtml(""); + ui->versionSelectionBox->clear(); + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + suggestCurrent(); +} + +void Page::onVersionSelectionItemChanged(QString data) +{ + if(data.isNull() || data.isEmpty()) + { + selectedVersion = ""; + return; + } + + selectedVersion = data; + suggestCurrent(); +} + +void Page::onSortingSelectionChanged(QString data) +{ + FilterModel::Sorting toSet = publicFilterModel->getAvailableSortings().value(data); + publicFilterModel->setSorting(toSet); + thirdPartyFilterModel->setSorting(toSet); + privateFilterModel->setSorting(toSet); +} + +void Page::onTabChanged(int tab) +{ + if(tab == 1) + { + currentModel = thirdPartyFilterModel; + currentList = ui->thirdPartyPackList; + currentModpackInfo = ui->thirdPartyPackDescription; + } + else if(tab == 2) + { + currentModel = privateFilterModel; + currentList = ui->privatePackList; + currentModpackInfo = ui->privatePackDescription; + } + else + { + currentModel = publicFilterModel; + currentList = ui->publicPackList; + currentModpackInfo = ui->publicPackDescription; + } + + currentList->selectionModel()->reset(); + QModelIndex idx = currentList->currentIndex(); + if(idx.isValid()) + { + auto pack = currentModel->data(idx, Qt::UserRole).value(); + onPackSelectionChanged(&pack); + } + else + { + onPackSelectionChanged(); + } +} + +void Page::onAddPackClicked() +{ + bool ok; + QString text = QInputDialog::getText( + this, + tr("Add FTB pack"), + tr("Enter pack code:"), + QLineEdit::Normal, + QString(), + &ok + ); + if(ok && !text.isEmpty()) + { + ftbPrivatePacks->add(text); + ftbFetchTask->fetchPrivate({text}); + } +} + +void Page::onRemovePackClicked() +{ + auto index = ui->privatePackList->currentIndex(); + if(!index.isValid()) + { + return; + } + auto row = index.row(); + Modpack pack = privateListModel->at(row); + auto answer = QMessageBox::question( + this, + tr("Remove pack"), + tr("Are you sure you want to remove pack %1?").arg(pack.name), + QMessageBox::Yes | QMessageBox::No + ); + if(answer != QMessageBox::Yes) + { + return; + } + + ftbPrivatePacks->remove(pack.packCode); + privateListModel->remove(row); + onPackSelectionChanged(); +} + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/Page.h b/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/Page.h new file mode 100644 index 0000000..d8225e1 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/Page.h @@ -0,0 +1,119 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +#include "ui/pages/BasePage.h" +#include +#include "tasks/Task.h" +#include "modplatform/legacy_ftb/PackHelpers.h" +#include "modplatform/legacy_ftb/PackFetchTask.h" +#include "QObjectPtr.h" + +class NewInstanceDialog; + +namespace LegacyFTB { + +namespace Ui +{ +class Page; +} + +class ListModel; +class FilterModel; +class PrivatePackListModel; +class PrivatePackFilterModel; +class PrivatePackManager; + +class Page : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit Page(NewInstanceDialog * dialog, QWidget *parent = 0); + virtual ~Page(); + QString displayName() const override + { + return tr("FTB Legacy"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("ftb_logo"); + } + QString id() const override + { + return "legacy_ftb"; + } + QString helpPage() const override + { + return "FTB-platform"; + } + bool shouldDisplay() const override; + void openedImpl() override; + +private: + void suggestCurrent(); + void onPackSelectionChanged(Modpack *pack = nullptr); + +private slots: + void ftbPackDataDownloadSuccessfully(ModpackList publicPacks, ModpackList thirdPartyPacks); + void ftbPackDataDownloadFailed(QString reason); + + void ftbPrivatePackDataDownloadSuccessfully(Modpack pack); + void ftbPrivatePackDataDownloadFailed(QString reason, QString packCode); + + void onSortingSelectionChanged(QString data); + void onVersionSelectionItemChanged(QString data); + + void onPublicPackSelectionChanged(QModelIndex first, QModelIndex second); + void onThirdPartyPackSelectionChanged(QModelIndex first, QModelIndex second); + void onPrivatePackSelectionChanged(QModelIndex first, QModelIndex second); + + void onTabChanged(int tab); + + void onAddPackClicked(); + void onRemovePackClicked(); + +private: + FilterModel* currentModel = nullptr; + QTreeView* currentList = nullptr; + QTextBrowser* currentModpackInfo = nullptr; + + bool initialized = false; + Modpack selected; + QString selectedVersion; + + ListModel* publicListModel = nullptr; + FilterModel* publicFilterModel = nullptr; + + ListModel *thirdPartyModel = nullptr; + FilterModel *thirdPartyFilterModel = nullptr; + + ListModel *privateListModel = nullptr; + FilterModel *privateFilterModel = nullptr; + + unique_qobject_ptr ftbFetchTask; + std::unique_ptr ftbPrivatePacks; + + NewInstanceDialog* dialog = nullptr; + + Ui::Page *ui = nullptr; +}; + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/Page.ui b/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/Page.ui new file mode 100644 index 0000000..367e0e8 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/legacy_ftb/Page.ui @@ -0,0 +1,135 @@ + + + LegacyFTB::Page + + + + 0 + 0 + 709 + 602 + + + + + + + 0 + + + + Public + + + + + + + 16777215 + 16777215 + + + + true + + + + + + + + + + + 3rd Party + + + + + + + + + + 16777215 + 16777215 + + + + true + + + + + + + + Private + + + + + + + 16777215 + 16777215 + + + + true + + + + + + + Add pack + + + + + + + Remove selected pack + + + + + + + + + + + + + + + + + + + + 265 + 0 + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + diff --git a/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthData.h b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthData.h new file mode 100644 index 0000000..509351c --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthData.h @@ -0,0 +1,66 @@ +/* + * Copyright 2022 kb1000 + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace Modrinth { +enum class LoadState { + NotLoaded = 0, + Loaded = 1, + Errored = 2 +}; + +enum class VersionType { + Alpha, + Beta, + Release, + Unknown +}; + +struct Download { + bool valid = false; + QString filename; + QString url; + QString sha1; + uint64_t size = 0; + bool primary = false; +}; + +struct Version { + QString name; + Download download; + QDateTime released; + VersionType type = VersionType::Unknown; + bool featured = false; +}; + +struct Modpack { + QString id; + + QString name; + QUrl iconUrl; + QString author; + QString description; + + LoadState detailsLoaded = LoadState::NotLoaded; + QString wikiUrl; + QString body; + + LoadState versionsLoaded = LoadState::NotLoaded; + QVector versions; +}; +} + +Q_DECLARE_METATYPE(Modrinth::Download) +Q_DECLARE_METATYPE(Modrinth::Version) +Q_DECLARE_METATYPE(Modrinth::Modpack) diff --git a/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthDocument.cpp b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthDocument.cpp new file mode 100644 index 0000000..f734610 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthDocument.cpp @@ -0,0 +1,80 @@ +/* + * Copyright 2022 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include "ModrinthDocument.h" + +#include +#include +#include +#include +#include + +Modrinth::ModrinthDocument::ModrinthDocument(const QString &markdown, QObject* parent) : QTextDocument(parent) { + HoeDown hoedown; + // 100 MiB + QPixmapCache::setCacheLimit(102400); + setHtml(hoedown.process(markdown.toUtf8())); +} + +QVariant Modrinth::ModrinthDocument::loadResource(int type, const QUrl& name) { + if(type == QTextDocument::ResourceType::ImageResource) { + auto pixmap = QPixmapCache::find(name.toString()); + if(!pixmap) { + requestResource(name); + return QVariant(); + } + return QVariant(*pixmap); + } + return QTextDocument::loadResource(type, name); +} + +void Modrinth::ModrinthDocument::downloadFinished(const QString& key, const QPixmap& out) { + m_loading.remove(key); + QPixmapCache::insert(key, out); + emit layoutUpdateRequired(); +} + +void Modrinth::ModrinthDocument::downloadFailed(const QString& key) { + m_failed.append(key); + m_loading.remove(key); +} + +void Modrinth::ModrinthDocument::requestResource(const QUrl& url) { + QString key = url.toString(); + if(m_loading.contains(key) || m_failed.contains(key)) + { + return; + } + + qDebug() << "Loading resource" << key; + + ImageLoad *load = new ImageLoad; + load->job = new NetJob(QString("Modrinth Image Download %1").arg(key), APPLICATION->network()); + load->job->addNetAction(Net::Download::makeByteArray(url, &load->output)); + load->key = key; + + QObject::connect(load->job.get(), &NetJob::succeeded, this, [this, load] { + QPixmap pixmap; + if(!pixmap.loadFromData(load->output)) { + qDebug() << load->output; + downloadFailed(load->key); + } + if(pixmap.width() > 800) { + pixmap = pixmap.scaledToWidth(800); + } + downloadFinished(load->key, pixmap); + }); + + QObject::connect(load->job.get(), &NetJob::failed, this, [this, load] + { + downloadFailed(load->key); + }); + + load->job->start(); + + m_loading[key] = load; +} diff --git a/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthDocument.h b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthDocument.h new file mode 100644 index 0000000..218a59a --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthDocument.h @@ -0,0 +1,45 @@ +/* + * Copyright 2022 Petr Mrázek + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include +#include + +namespace Modrinth { + +using Callback = std::function; +struct ImageLoad { + QString key; + NetJob::Ptr job; + QByteArray output; + Callback handler; +}; + +class ModrinthDocument: public QTextDocument { + Q_OBJECT +public: + ModrinthDocument(const QString &markdown, QObject * parent = nullptr); + +signals: + void layoutUpdateRequired(); + +protected: + QVariant loadResource(int type, const QUrl & name) override; + +private: + void downloadFailed(const QString &key); + void downloadFinished(const QString &key, const QPixmap &out); + + void requestResource(const QUrl &url); + +private: + QMap m_loading; + QStringList m_failed; +}; + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp new file mode 100644 index 0000000..3c436d2 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthModel.cpp @@ -0,0 +1,575 @@ +/* + * Copyright 2013-2022 MultiMC Contributors + * Copyright 2022 kb1000 + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#include "ModrinthModel.h" +#include "Application.h" +#include "Json.h" + +#include + +Modrinth::ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +Modrinth::ListModel::~ListModel() = default; + +QVariant Modrinth::ListModel::data(const QModelIndex &index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + auto pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.id)) + { + return (m_logoMap.value(pack.id)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ListModel *)this)->requestLogo(pack.id, pack.iconUrl); + return icon; + } + else if (role == Qt::ToolTipRole) + { + return pack.description; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + return QVariant(); +} + +bool Modrinth::ListModel::canFetchMore(const QModelIndex& parent) const +{ + return searchState == CanPossiblyFetchMore; +} + + +void Modrinth::ListModel::fetchMore(const QModelIndex& parent) +{ + if (parent.isValid()) + return; + if(nextSearchOffset == 0) { + qWarning() << "fetchMore with 0 offset is wrong..."; + return; + } + performPaginatedSearch(); +} + +int Modrinth::ListModel::columnCount(const QModelIndex &parent) const +{ + return 1; +} + +int Modrinth::ListModel::rowCount(const QModelIndex &parent) const +{ + return modpacks.size(); +} + +void Modrinth::ListModel::searchWithTerm(const QString& term, const QString &sort) +{ + if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull() && currentSort == sort) { + return; + } + currentSearchTerm = term; + currentSort = sort; + if(jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } + else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + nextSearchOffset = 0; + performPaginatedSearch(); +} + +void Modrinth::ListModel::performPaginatedSearch() +{ + auto *netJob = new NetJob("Modrinth::Search", APPLICATION->network()); + QString searchUrl = ""; + if (currentSearchTerm.isEmpty()) { + searchUrl = QString("https://api.modrinth.com/v2/search?facets=[[%22project_type:modpack%22]]&index=%1&limit=25&offset=%2").arg(currentSort).arg(nextSearchOffset); + } + else + { + searchUrl = QString( + "https://api.modrinth.com/v2/search?facets=[[%22project_type:modpack%22]]&index=%1&limit=25&offset=%2&query=%3" + ).arg(currentSort).arg(nextSearchOffset).arg(currentSearchTerm); + } + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(); + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); +} + +void Modrinth::ListModel::searchRequestFinished() +{ + jobPtr.reset(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Modrinth at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + QVector newList; + QJsonArray hits; + int total_hits = 0; + + try + { + auto obj = Json::requireObject(doc); + hits = Json::requireArray(obj, "hits"); + total_hits = Json::requireInteger(obj, "total_hits"); + } + catch(const JSONValidationError &e) + { + qWarning() << "Error while parsing response from Modrinth: " << e.cause(); + return; + } + + for (auto packRaw : hits) + { + auto packObj = packRaw.toObject(); + Modrinth::Modpack pack; + try + { + if (Json::ensureString(packObj, "client_side", "required") == QStringLiteral("unsupported")) + continue; + pack.id = Json::requireString(packObj, "project_id"); + pack.name = Json::requireString(packObj, "title"); + pack.iconUrl = Json::requireUrl(packObj, "icon_url"); + pack.author = Json::requireString(packObj, "author"); + pack.description = Json::requireString(packObj, "description"); + newList.append(pack); + } + catch(const JSONValidationError &e) + { + qWarning() << "Error while loading pack from Modrinth: " << e.cause(); + continue; + } + } + + if ((total_hits - nextSearchOffset) <= 25) + searchState = Finished; + else + { + nextSearchOffset += 25; + searchState = CanPossiblyFetchMore; + } + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + // TODO: when we update from Qt 5.4, just use append(QVector) + for(auto item: newList) { + modpacks.append(item); + } + endInsertRows(); +} + +void Modrinth::ListModel::searchRequestFailed() +{ + jobPtr.reset(); + + if(searchState == ResetRequested) + { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + nextSearchOffset = 0; + performPaginatedSearch(); + } + else + { + searchState = Finished; + } +} + +void Modrinth::ListModel::logoLoaded(const QString &logo, const QIcon &out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, out); + for(int i = 0; i < modpacks.size(); i++) { + if(modpacks[i].id == logo) { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + } + } +} + +void Modrinth::ListModel::logoFailed(const QString &logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void Modrinth::ListModel::requestLogo(const QString &logo, const QUrl &url) +{ + if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo)) + { + return; + } + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(logo.section(".", 0, 0))); + auto *job = new NetJob(QString("Modrinth Icon Download %1").arg(logo), APPLICATION->network()); + job->addNetAction(Net::Download::makeCached(url, entry)); + + auto fullPath = entry->getFullPath(); + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath] + { + QIcon icon(fullPath); + QSize size = icon.actualSize(QSize(48, 48)); + if (size.width() < 48 && size.height() < 48) + { + icon = icon.pixmap(48, 48).scaled(48,48, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + } + logoLoaded(logo, icon); + if(waitingCallbacks.contains(logo)) + { + waitingCallbacks.value(logo)(fullPath); + } + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo] + { + logoFailed(logo); + }); + + job->start(); + + m_loadingLogos.append(logo); +} + +void Modrinth::ListModel::getPackDetails(const QString& id) +{ + auto index = getIndexFromId(id); + if(!index) { + return; + } + + if(isPackDetailInProgress()) { + queuedPackDetailRequest = id; + cancelPackDetail(); + return; + } + + currentPackDetailRequest = id; + + QString detailsUrl = "https://api.modrinth.com/v2/project/" + id; + + auto & modpack = modpacks[*index]; + if(modpack.detailsLoaded != LoadState::Loaded) + { + auto *netJob = new NetJob("Modrinth::PackDetails", APPLICATION->network()); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(detailsUrl), &detailsResponse)); + detailsPtr = netJob; + detailsPtr->start(); + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::detailsRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::detailsRequestFailed); + } + + QString versionsUrl = detailsUrl + "/version"; + if(modpack.versionsLoaded != LoadState::Loaded) + { + auto *netJob = new NetJob("Modrinth::PackVersions", APPLICATION->network()); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(versionsUrl), &versionsResponse)); + versionsPtr = netJob; + versionsPtr->start(); + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::versionsRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::versionsRequestFailed); + } +} + +bool Modrinth::ListModel::isPackDetailInProgress() +{ + return detailsPtr || versionsPtr; +} + +void Modrinth::ListModel::cancelPackDetail() +{ + if(detailsPtr) { + detailsPtr->abort(); + } + if(versionsPtr) { + versionsPtr->abort(); + } +} + +nonstd::optional Modrinth::ListModel::getIndexFromId(const QString& id) +{ + for(int i = 0; i < modpacks.size(); i++) { + if(modpacks[i].id == id) { + return i; + } + } + return nonstd::nullopt; +} + +nonstd::optional Modrinth::ListModel::getModpackById(const QString& id) +{ + auto index = getIndexFromId(id); + if(!index) { + return nonstd::nullopt; + } + return modpacks[*index]; +} + +namespace { +bool parseDetailsInto(QByteArray & input, Modrinth::Modpack& output) { + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(input, &parse_error); + if(parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing pack details response from Modrinth at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << input; + return false; + } + + try + { + auto obj = Json::requireObject(doc); + QString body = Json::requireString(obj, "body"); + output.body = body; + return true; + } + catch(const JSONValidationError &e) + { + qWarning() << "Error while parsing response from Modrinth: " << e.cause(); + return false; + } +} +} + +void Modrinth::ListModel::detailsRequestFinished() +{ + auto index = getIndexFromId(currentPackDetailRequest); + if(index) { + auto & modpack = modpacks[*index]; + + if(parseDetailsInto(detailsResponse, modpack)) { + modpack.detailsLoaded = LoadState::Loaded; + } + else { + modpack.detailsLoaded = LoadState::Errored; + } + emit packDataChanged(currentPackDetailRequest); + } + detailsPtr.reset(); + checkDetailsDone(); +} + +void Modrinth::ListModel::detailsRequestFailed() +{ + auto index = getIndexFromId(currentPackDetailRequest); + if(index) { + auto & modpack = modpacks[*index]; + if(modpack.detailsLoaded == LoadState::NotLoaded) { + modpack.detailsLoaded = LoadState::Errored; + emit packDataChanged(currentPackDetailRequest); + } + } + detailsPtr.reset(); + checkDetailsDone(); +} + +/* + { + "id": "8mMRnfwS", + "project_id": "WCJmvhgU", + "author_id": "akScBBW1", + "featured": true, + "name": "Third Release", + "version_number": "2022.1.12", + "changelog": "This is the third release!", + "changelog_url": null, + "date_published": "2022-01-12T20:41:27+00:00", + "downloads": 22, + "version_type": "release", + "files": [ + { + "hashes": { + "sha1": "0fe87efacfd25c4c5e011cd1433e9be494b23b1c", + "sha512": "d43e148d35d0267b49ed14a7bb4bb5879aa3ebbf5805c2fe125f0f0f19fd84994242bdcd568399c9ff80ecfbe0e975ab632d8c71773bc09d54cfc857f9a6f716" + }, + "url": "https://cdn.modrinth.com/data/WCJmvhgU/versions/2022.1.12/waffles_Modpack-2022.1.12no-hydrogen.mrpack", + "filename": "waffles_Modpack-2022.1.12no-hydrogen.mrpack", + "primary": false, + "size": 0 + } + ], + "dependencies": [], + "game_versions": [ + "1.18.1" + ], + "loaders": [ + "fabric" + ] + } + */ + +bool parseFile(QJsonObject & fileObj, Modrinth::Download & out) { + out.primary = Json::requireBoolean(fileObj, "primary"); + out.size = Json::requireInteger(fileObj, "size"); + out.url = Json::requireString(fileObj, "url"); + out.filename = Json::requireString(fileObj, "filename"); + + auto hashesObj = fileObj["hashes"].toObject(); + out.sha1 = Json::requireString(hashesObj, "sha1"); + + if(!out.filename.endsWith(".mrpack")) { + out.valid = false; + return false; + } + else { + out.valid = true; + return true; + } +} + +namespace { + +bool parseVersionsInto(QByteArray & input, Modrinth::Modpack& output) { + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(input, &parse_error); + if(parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing pack versions response from Modrinth at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << input; + return false; + } + + qDebug() << input; + + try + { + QVector newList; + QJsonArray versions = Json::requireArray(doc); + for (auto obj : versions) + { + auto packObj = obj.toObject(); + Modrinth::Version version; + try + { + if (Json::ensureString(packObj, "client_side", "required") == QStringLiteral("unsupported")) + continue; + version.name = Json::requireString(packObj, "version_number"); + version.released = Json::requireDateTime(packObj, "date_published"); + version.featured = Json::requireBoolean(packObj, "featured"); + auto versionTypeString = Json::requireString(packObj, "version_type"); + if(versionTypeString == "alpha") { + version.type = Modrinth::VersionType::Alpha; + } + else if(versionTypeString == "beta") { + version.type = Modrinth::VersionType::Beta; + } + else if (versionTypeString == "release") { + version.type = Modrinth::VersionType::Release; + } + else { + qWarning() << "Unknown version type of Modrinth modpack: " << versionTypeString; + version.type = Modrinth::VersionType::Unknown; + } + Modrinth::Download fallbackOut = {}; + auto filesArray = Json::requireArray(packObj, "files"); + for(int i = 0; i < filesArray.size(); i++) { + Modrinth::Download maybeFileOut = {}; + QJsonObject fileObj = filesArray[i].toObject(); + parseFile(fileObj, maybeFileOut); + if(i == 0) { + fallbackOut = maybeFileOut; + } + if(maybeFileOut.valid && maybeFileOut.primary) { + version.download = maybeFileOut; + break; + } + } + + if(!version.download.valid) { + version.download = fallbackOut; + } + + if(version.download.valid) { + newList.append(version); + } + } + catch(const JSONValidationError &e) + { + qWarning() << "Error while loading pack from Modrinth: " << e.cause(); + continue; + } + } + output.versions = newList; + return true; + } + catch(const JSONValidationError &e) + { + qWarning() << "Error while parsing response from Modrinth: " << e.cause(); + return false; + } +} +} + +void Modrinth::ListModel::versionsRequestFinished() +{ + auto index = getIndexFromId(currentPackDetailRequest); + if(index) { + auto & modpack = modpacks[*index]; + parseVersionsInto(versionsResponse, modpack); + modpack.versionsLoaded = LoadState::Loaded; + emit packDataChanged(currentPackDetailRequest); + } + versionsPtr.reset(); + checkDetailsDone(); +} + +void Modrinth::ListModel::versionsRequestFailed() +{ + auto index = getIndexFromId(currentPackDetailRequest); + if(index) { + auto & modpack = modpacks[*index]; + if(modpack.versionsLoaded == LoadState::NotLoaded) { + modpack.versionsLoaded = LoadState::Errored; + emit packDataChanged(currentPackDetailRequest); + } + } + versionsPtr.reset(); + checkDetailsDone(); +} + +void Modrinth::ListModel::checkDetailsDone() +{ + if(isPackDetailInProgress()) { + return; + } + + // all detail requests are finished + currentPackDetailRequest.clear(); + + // is there a new one queued? + if(!queuedPackDetailRequest.isNull()) { + getPackDetails(queuedPackDetailRequest); + queuedPackDetailRequest.clear(); + } +} diff --git a/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h new file mode 100644 index 0000000..1c4fd1a --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthModel.h @@ -0,0 +1,94 @@ +/* + * Copyright 2013-2022 MultiMC Contributors + * Copyright 2022 kb1000 + * + * This source is subject to the Microsoft Permissive License (MS-PL). + * Please see the COPYING.md file for more information. + */ + +#pragma once + +#include "ModrinthData.h" +#include "net/NetJob.h" + +#include +#include +#include + +namespace Modrinth { + +using LogoCallback = std::function; + +class ListModel : public QAbstractListModel { + Q_OBJECT + +public: + explicit ListModel(QObject *parent); + ~ListModel() override; + + QVariant data(const QModelIndex &index, int role) const override; + int columnCount(const QModelIndex &parent) const override; + int rowCount(const QModelIndex &parent) const override; + bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; + + void searchWithTerm(const QString &term, const QString &sort); + void getPackDetails(const QString &id); + nonstd::optional getModpackById(const QString &id); + +signals: + void packDataChanged(const QString &id); + +private slots: + void searchRequestFinished(); + void searchRequestFailed(); + + void detailsRequestFinished(); + void detailsRequestFailed(); + + void versionsRequestFinished(); + void versionsRequestFailed(); + +private: + void performPaginatedSearch(); + + void logoFailed(const QString &logo); + void logoLoaded(const QString &logo, const QIcon &out); + + void requestLogo(const QString &logo, const QUrl &url); + + nonstd::optional getIndexFromId(const QString &id); + + bool isPackDetailInProgress(); + void cancelPackDetail(); + void checkDetailsDone(); + + QVector modpacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + QMap m_logoMap; + QMap waitingCallbacks; + + QString currentSearchTerm; + QString currentSort = QStringLiteral("relevance"); + int nextSearchOffset = 0; + enum SearchState { + None, + CanPossiblyFetchMore, + ResetRequested, + Finished + } searchState = None; + + QString queuedPackDetailRequest; + QString currentPackDetailRequest; + NetJob::Ptr jobPtr; + QByteArray response; + + NetJob::Ptr detailsPtr; + QByteArray detailsResponse; + + NetJob::Ptr versionsPtr; + QByteArray versionsResponse; +}; + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp new file mode 100644 index 0000000..7079318 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthPage.cpp @@ -0,0 +1,199 @@ +/* + * Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModrinthModel.h" +#include "ModrinthPage.h" +#include "ModrinthDocument.h" +#include "ui/dialogs/NewInstanceDialog.h" + +#include "ui_ModrinthPage.h" + +#include +#include + +ModrinthPage::ModrinthPage(NewInstanceDialog *dialog, QWidget *parent) : QWidget(parent), ui(new Ui::ModrinthPage), dialog(dialog) +{ + ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &ModrinthPage::triggerSearch); + ui->searchEdit->installEventFilter(this); + model = new Modrinth::ListModel(this); + ui->packView->setModel(model); + + ui->versionSelectionBox->view()->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + ui->versionSelectionBox->view()->parentWidget()->setMaximumHeight(300); + + ui->sortByBox->addItem(tr("Sort by relevance"), QStringLiteral("relevance")); + ui->sortByBox->addItem(tr("Sort by total downloads"), QStringLiteral("downloads")); + ui->sortByBox->addItem(tr("Sort by follow count"), QStringLiteral("follows")); + ui->sortByBox->addItem(tr("Sort by creation date"), QStringLiteral("newest")); + ui->sortByBox->addItem(tr("Sort by last updated"), QStringLiteral("updated")); + + connect(ui->sortByBox, SIGNAL(currentIndexChanged(int)), this, SLOT(triggerSearch())); + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &ModrinthPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &ModrinthPage::onVersionSelectionChanged); + connect(model, &Modrinth::ListModel::packDataChanged, this, &ModrinthPage::onPackDataChanged); +} + +ModrinthPage::~ModrinthPage() +{ + delete ui; +} + +void ModrinthPage::openedImpl() +{ + BasePage::openedImpl(); + triggerSearch(); +} + +bool ModrinthPage::eventFilter(QObject *watched, QEvent *event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + auto *keyEvent = reinterpret_cast(event); + if (keyEvent->key() == Qt::Key_Return) { + this->triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QObject::eventFilter(watched, event); +} + +void ModrinthPage::triggerSearch() { + model->searchWithTerm(ui->searchEdit->text(), ui->sortByBox->itemData(ui->sortByBox->currentIndex()).toString()); +} + +void ModrinthPage::onSelectionChanged(QModelIndex first, QModelIndex second) { + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + //ui->frame->clear(); + return; + } + + current = model->data(first, Qt::UserRole).value(); + model->getPackDetails(current.id); + updateCurrentPackUI(); + suggestCurrent(); +} + +void ModrinthPage::onVersionSelectionChanged(const QString& version) { + if(version.isEmpty() || ui->versionSelectionBox->count() == 0) { + currentVersion = Modrinth::Version(); + } + else { + currentVersion = ui->versionSelectionBox->currentData().value(); + } + suggestCurrent(); +} + +void ModrinthPage::suggestCurrent() +{ + if(!isOpened) + { + return; + } + + if (!currentVersion.name.size()) + { + dialog->setSuggestedPack(); + return; + } + + dialog->setSuggestedPack(current.name + " " + currentVersion.name, new InstanceImportTask(currentVersion.download.url)); + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("ModrinthPacks", QString("logos/%1").arg(current.id)); + dialog->setSuggestedIconFromFile(entry->getFullPath(), QString("modrinth-%1").arg(current.id)); +} + +void ModrinthPage::onPackDataChanged(const QString& id) +{ + if(id != current.id) { + return; + } + auto newData = model->getModpackById(id); + if(newData) { + current = *newData; + updateCurrentPackUI(); + } +} + +QString versionToString(const Modrinth::Version& version) { + switch(version.type) { + case Modrinth::VersionType::Alpha: { + return QString("%1 (Alpha)").arg(version.name); + } + case Modrinth::VersionType::Beta: { + return QString("%1 (Beta)").arg(version.name); + } + case Modrinth::VersionType::Release: { + return version.name; + } + case Modrinth::VersionType::Unknown: { + break; + } + } + return QString("%1 (?)").arg(version.name); +} + +void ModrinthPage::updateCurrentPackUI() +{ + switch(current.detailsLoaded) { + case Modrinth::LoadState::Errored: { + ui->packDescription->setText(tr("Failed to get Modrinth modpack details...")); + break; + } + case Modrinth::LoadState::NotLoaded: { + ui->packDescription->setText(tr("Loading...")); + break; + } + case Modrinth::LoadState::Loaded: { + auto document = new Modrinth::ModrinthDocument(current.body); + connect(document, &Modrinth::ModrinthDocument::layoutUpdateRequired, this, &ModrinthPage::forceDocumentLayout); + ui->packDescription->setDocument(document); + break; + } + } + if(current.versions.size() == 0) { + ui->versionSelectionBox->clear(); + } + else { + ui->versionSelectionBox->clear(); + int releaseFound = -1; + int i = 0; + for(auto & version: current.versions) { + ui->versionSelectionBox->addItem(versionToString(version), QVariant::fromValue(version)); + if(releaseFound == -1 && version.type == Modrinth::VersionType::Release) { + releaseFound = i; + } + i++; + } + if(releaseFound != -1) { + ui->versionSelectionBox->setCurrentIndex(releaseFound); + } + else if(current.versions.size() != 0) { + ui->versionSelectionBox->setCurrentIndex(0); + } + // select first release found from the top + } + suggestCurrent(); +} + +void ModrinthPage::forceDocumentLayout() { + ui->packDescription->document()->adjustSize(); +} diff --git a/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h new file mode 100644 index 0000000..ddaa2db --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthPage.h @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-2022 kb1000 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "Application.h" +#include "ui/pages/BasePage.h" +#include "ModrinthData.h" + +#include + +namespace Ui +{ + class ModrinthPage; +} + +class NewInstanceDialog; + +namespace Modrinth { + class ListModel; +} + +class ModrinthPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit ModrinthPage(NewInstanceDialog *dialog, QWidget *parent = nullptr); + ~ModrinthPage() override; + + QString displayName() const override + { + return tr("Modrinth"); + } + QIcon icon() const override + { + return APPLICATION->getThemedIcon("modrinth"); + } + QString id() const override + { + return "modrinth"; + } + + void openedImpl() override; + + bool eventFilter(QObject *watched, QEvent *event) override; + +private slots: + void triggerSearch(); + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onVersionSelectionChanged(const QString & version); + void onPackDataChanged(const QString &id); + void forceDocumentLayout(); + +private: + void updateCurrentPackUI(); + void suggestCurrent(); + + Ui::ModrinthPage *ui; + NewInstanceDialog *dialog; + Modrinth::ListModel *model = nullptr; + Modrinth::Modpack current; + Modrinth::Version currentVersion; +}; diff --git a/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui new file mode 100644 index 0000000..08dcc39 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/modrinth/ModrinthPage.ui @@ -0,0 +1,97 @@ + + + ModrinthPage + + + + 0 + 0 + 837 + 685 + + + + + + + + + Search and filter ... + + + + + + + Search + + + + + + + + + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + true + + + + 48 + 48 + + + + + + + + true + + + true + + + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + searchEdit + searchButton + packView + packDescription + sortByBox + versionSelectionBox + + + + diff --git a/ultimmc/launcher/ui/pages/modplatform/technic/TechnicData.h b/ultimmc/launcher/ui/pages/modplatform/technic/TechnicData.h new file mode 100644 index 0000000..e92cda1 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/technic/TechnicData.h @@ -0,0 +1,49 @@ +/* Copyright 2020-2021 MultiMC Contributors + * Copyright 2021-2022 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include + +namespace Technic { +struct Modpack { + QString slug; + + QString name; + QString logoUrl; + QString logoName; + + bool broken = true; + + QString url; + bool isSolder = false; + QString minecraftVersion; + + bool metadataLoaded = false; + QString websiteUrl; + QString author; + QString description; + QString currentVersion; + + bool versionsLoaded = false; + QString recommended; + QVector versions; +}; +} + +Q_DECLARE_METATYPE(Technic::Modpack) diff --git a/ultimmc/launcher/ui/pages/modplatform/technic/TechnicModel.cpp b/ultimmc/launcher/ui/pages/modplatform/technic/TechnicModel.cpp new file mode 100644 index 0000000..b0128b9 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/technic/TechnicModel.cpp @@ -0,0 +1,282 @@ +/* Copyright 2020-2021 MultiMC Contributors + * Copyright 2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "TechnicModel.h" +#include "Application.h" +#include "BuildConfig.h" +#include "Json.h" + +#include + +Technic::ListModel::ListModel(QObject *parent) : QAbstractListModel(parent) +{ +} + +Technic::ListModel::~ListModel() +{ +} + +QVariant Technic::ListModel::data(const QModelIndex& index, int role) const +{ + int pos = index.row(); + if(pos >= modpacks.size() || pos < 0 || !index.isValid()) + { + return QString("INVALID INDEX %1").arg(pos); + } + + Modpack pack = modpacks.at(pos); + if(role == Qt::DisplayRole) + { + return pack.name; + } + else if(role == Qt::DecorationRole) + { + if(m_logoMap.contains(pack.logoName)) + { + return (m_logoMap.value(pack.logoName)); + } + QIcon icon = APPLICATION->getThemedIcon("screenshot-placeholder"); + ((ListModel *)this)->requestLogo(pack.logoName, pack.logoUrl); + return icon; + } + else if(role == Qt::UserRole) + { + QVariant v; + v.setValue(pack); + return v; + } + return QVariant(); +} + +int Technic::ListModel::columnCount(const QModelIndex&) const +{ + return 1; +} + +int Technic::ListModel::rowCount(const QModelIndex&) const +{ + return modpacks.size(); +} + +void Technic::ListModel::searchWithTerm(const QString& term) +{ + if(currentSearchTerm == term && currentSearchTerm.isNull() == term.isNull()) { + return; + } + currentSearchTerm = term; + if(jobPtr) { + jobPtr->abort(); + searchState = ResetRequested; + return; + } + else { + beginResetModel(); + modpacks.clear(); + endResetModel(); + searchState = None; + } + performSearch(); +} + +void Technic::ListModel::performSearch() +{ + NetJob *netJob = new NetJob("Technic::Search", APPLICATION->network()); + QString searchUrl = ""; + if (currentSearchTerm.isEmpty()) { + searchUrl = QString("%1trending?build=%2") + .arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD); + searchMode = List; + } + else if (currentSearchTerm.startsWith("http://api.technicpack.net/modpack/")) { + searchUrl = QString("https://%1?build=%2") + .arg(currentSearchTerm.mid(7), BuildConfig.TECHNIC_API_BUILD); + searchMode = Single; + } + else if (currentSearchTerm.startsWith("https://api.technicpack.net/modpack/")) { + searchUrl = QString("%1?build=%2").arg(currentSearchTerm, BuildConfig.TECHNIC_API_BUILD); + searchMode = Single; + } + else { + searchUrl = QString( + "%1search?build=%2&q=%3" + ).arg(BuildConfig.TECHNIC_API_BASE_URL, BuildConfig.TECHNIC_API_BUILD, currentSearchTerm); + searchMode = List; + } + netJob->addNetAction(Net::Download::makeByteArray(QUrl(searchUrl), &response)); + jobPtr = netJob; + jobPtr->start(); + QObject::connect(netJob, &NetJob::succeeded, this, &ListModel::searchRequestFinished); + QObject::connect(netJob, &NetJob::failed, this, &ListModel::searchRequestFailed); +} + +void Technic::ListModel::searchRequestFinished() +{ + jobPtr.reset(); + + QJsonParseError parse_error; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + if(parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + return; + } + + QList newList; + try { + auto root = Json::requireObject(doc); + + switch (searchMode) { + case List: { + auto objs = Json::requireArray(root, "modpacks"); + for (auto technicPack: objs) { + Modpack pack; + auto technicPackObject = Json::requireValueObject(technicPack); + pack.name = Json::requireString(technicPackObject, "name"); + pack.slug = Json::requireString(technicPackObject, "slug"); + if (pack.slug == "vanilla") + continue; + + auto rawURL = Json::ensureString(technicPackObject, "iconUrl", "null"); + if(rawURL == "null") { + pack.logoUrl = "null"; + pack.logoName = "null"; + } + else { + pack.logoUrl = rawURL; + pack.logoName = rawURL.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + } + pack.broken = false; + newList.append(pack); + } + break; + } + case Single: { + if (root.contains("error")) { + // Invalid API url + break; + } + + Modpack pack; + pack.name = Json::requireString(root, "displayName"); + pack.slug = Json::requireString(root, "name"); + + if (root.contains("icon")) { + auto iconObj = Json::requireObject(root, "icon"); + auto iconUrl = Json::requireString(iconObj, "url"); + + pack.logoUrl = iconUrl; + pack.logoName = iconUrl.section(QLatin1Char('/'), -1).section(QLatin1Char('.'), 0, 0); + } + else { + pack.logoUrl = "null"; + pack.logoName = "null"; + } + + pack.broken = false; + newList.append(pack); + break; + } + } + } + catch (const JSONValidationError &err) + { + qCritical() << "Couldn't parse technic search results:" << err.cause() ; + return; + } + searchState = Finished; + beginInsertRows(QModelIndex(), modpacks.size(), modpacks.size() + newList.size() - 1); + modpacks.append(newList); + endInsertRows(); +} + +void Technic::ListModel::getLogo(const QString& logo, const QString& logoUrl, Technic::LogoCallback callback) +{ + if(m_logoMap.contains(logo)) + { + callback(APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo))->getFullPath()); + } + else + { + requestLogo(logo, logoUrl); + } +} + +void Technic::ListModel::searchRequestFailed() +{ + jobPtr.reset(); + + if(searchState == ResetRequested) + { + beginResetModel(); + modpacks.clear(); + endResetModel(); + + performSearch(); + } + else + { + searchState = Finished; + } +} + + +void Technic::ListModel::logoLoaded(QString logo, QString out) +{ + m_loadingLogos.removeAll(logo); + m_logoMap.insert(logo, QIcon(out)); + for(int i = 0; i < modpacks.size(); i++) + { + if(modpacks[i].logoName == logo) + { + emit dataChanged(createIndex(i, 0), createIndex(i, 0), {Qt::DecorationRole}); + } + } +} + +void Technic::ListModel::logoFailed(QString logo) +{ + m_failedLogos.append(logo); + m_loadingLogos.removeAll(logo); +} + +void Technic::ListModel::requestLogo(QString logo, QString url) +{ + if(m_loadingLogos.contains(logo) || m_failedLogos.contains(logo) || logo == "null") + { + return; + } + + MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("TechnicPacks", QString("logos/%1").arg(logo)); + NetJob *job = new NetJob(QString("Technic Icon Download %1").arg(logo), APPLICATION->network()); + job->addNetAction(Net::Download::makeCached(QUrl(url), entry)); + + auto fullPath = entry->getFullPath(); + + QObject::connect(job, &NetJob::succeeded, this, [this, logo, fullPath] + { + logoLoaded(logo, fullPath); + }); + + QObject::connect(job, &NetJob::failed, this, [this, logo] + { + logoFailed(logo); + }); + + job->start(); + + m_loadingLogos.append(logo); +} diff --git a/ultimmc/launcher/ui/pages/modplatform/technic/TechnicModel.h b/ultimmc/launcher/ui/pages/modplatform/technic/TechnicModel.h new file mode 100644 index 0000000..d274881 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/technic/TechnicModel.h @@ -0,0 +1,75 @@ +/* Copyright 2020-2021 MultiMC Contributors + * Copyright 2021 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "TechnicData.h" +#include "net/NetJob.h" + +namespace Technic { + +typedef std::function LogoCallback; + +class ListModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ListModel(QObject *parent); + virtual ~ListModel(); + + virtual QVariant data(const QModelIndex& index, int role) const; + virtual int columnCount(const QModelIndex& parent) const; + virtual int rowCount(const QModelIndex& parent) const; + + void getLogo(const QString &logo, const QString &logoUrl, LogoCallback callback); + void searchWithTerm(const QString & term); + +private slots: + void searchRequestFinished(); + void searchRequestFailed(); + + void logoFailed(QString logo); + void logoLoaded(QString logo, QString out); + +private: + void performSearch(); + void requestLogo(QString logo, QString url); + +private: + QList modpacks; + QStringList m_failedLogos; + QStringList m_loadingLogos; + QMap m_logoMap; + QMap waitingCallbacks; + + QString currentSearchTerm; + enum SearchState { + None, + ResetRequested, + Finished + } searchState = None; + enum SearchMode { + List, + Single, + } searchMode = List; + NetJob::Ptr jobPtr; + QByteArray response; +}; + +} diff --git a/ultimmc/launcher/ui/pages/modplatform/technic/TechnicPage.cpp b/ultimmc/launcher/ui/pages/modplatform/technic/TechnicPage.cpp new file mode 100644 index 0000000..d05eadf --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/technic/TechnicPage.cpp @@ -0,0 +1,308 @@ +/* Copyright 2013-2022 MultiMC Contributors + * Copyright 2021-2022 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include "TechnicPage.h" +#include "ui_TechnicPage.h" + +#include + +#include "ui/dialogs/NewInstanceDialog.h" + +#include "BuildConfig.h" +#include "TechnicModel.h" +#include "modplatform/technic/SingleZipPackInstallTask.h" +#include "modplatform/technic/SolderPackInstallTask.h" +#include "modplatform/technic/SolderPackManifest.h" +#include "Json.h" + +#include "Application.h" + +TechnicPage::TechnicPage(NewInstanceDialog* dialog, QWidget *parent) + : QWidget(parent), ui(new Ui::TechnicPage), dialog(dialog) +{ + ui->setupUi(this); + connect(ui->searchButton, &QPushButton::clicked, this, &TechnicPage::triggerSearch); + ui->searchEdit->installEventFilter(this); + model = new Technic::ListModel(this); + ui->packView->setModel(model); + + connect(ui->packView->selectionModel(), &QItemSelectionModel::currentChanged, this, &TechnicPage::onSelectionChanged); + connect(ui->versionSelectionBox, &QComboBox::currentTextChanged, this, &TechnicPage::onVersionSelectionChanged); +} + +bool TechnicPage::eventFilter(QObject* watched, QEvent* event) +{ + if (watched == ui->searchEdit && event->type() == QEvent::KeyPress) { + QKeyEvent* keyEvent = static_cast(event); + if (keyEvent->key() == Qt::Key_Return) { + triggerSearch(); + keyEvent->accept(); + return true; + } + } + return QWidget::eventFilter(watched, event); +} + +TechnicPage::~TechnicPage() +{ + delete ui; +} + +bool TechnicPage::shouldDisplay() const +{ + return true; +} + +void TechnicPage::openedImpl() +{ + suggestCurrent(); + triggerSearch(); +} + +void TechnicPage::triggerSearch() { + model->searchWithTerm(ui->searchEdit->text()); +} + +void TechnicPage::onSelectionChanged(QModelIndex first, QModelIndex second) +{ + ui->versionSelectionBox->clear(); + + if(!first.isValid()) + { + if(isOpened) + { + dialog->setSuggestedPack(); + } + return; + } + + current = model->data(first, Qt::UserRole).value(); + suggestCurrent(); +} + +void TechnicPage::suggestCurrent() +{ + if (!isOpened) + { + return; + } + if (current.broken) + { + dialog->setSuggestedPack(); + return; + } + + QString editedLogoName = "technic_" + current.logoName.section(".", 0, 0); + model->getLogo(current.logoName, current.logoUrl, [this, editedLogoName](QString logo) + { + dialog->setSuggestedIconFromFile(logo, editedLogoName); + }); + + if (current.metadataLoaded) + { + metadataLoaded(); + return; + } + + NetJob *netJob = new NetJob(QString("Technic::PackMeta(%1)").arg(current.name), APPLICATION->network()); + QString slug = current.slug; + netJob->addNetAction(Net::Download::makeByteArray(QString("%1modpack/%2?build=%3").arg(BuildConfig.TECHNIC_API_BASE_URL, slug, BuildConfig.TECHNIC_API_BUILD), &response)); + QObject::connect(netJob, &NetJob::succeeded, this, [this, slug] + { + jobPtr.reset(); + + if (current.slug != slug) + { + return; + } + + QJsonParseError parse_error {}; + QJsonDocument doc = QJsonDocument::fromJson(response, &parse_error); + QJsonObject obj = doc.object(); + if(parse_error.error != QJsonParseError::NoError) + { + qWarning() << "Error while parsing JSON response from Technic at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << *response; + return; + } + if (!obj.contains("url")) + { + qWarning() << "Json doesn't contain an url key"; + return; + } + QJsonValueRef url = obj["url"]; + if (url.isString()) + { + current.url = url.toString(); + } + else + { + if (!obj.contains("solder")) + { + qWarning() << "Json doesn't contain a valid url or solder key"; + return; + } + QJsonValueRef solderUrl = obj["solder"]; + if (solderUrl.isString()) + { + current.url = solderUrl.toString(); + current.isSolder = true; + } + else + { + qWarning() << "Json doesn't contain a valid url or solder key"; + return; + } + } + + current.minecraftVersion = Json::ensureString(obj, "minecraft", QString(), "__placeholder__"); + current.websiteUrl = Json::ensureString(obj, "platformUrl", QString(), "__placeholder__"); + current.author = Json::ensureString(obj, "user", QString(), "__placeholder__"); + current.description = Json::ensureString(obj, "description", QString(), "__placeholder__"); + current.currentVersion = Json::ensureString(obj, "version", QString(), "__placeholder__"); + current.metadataLoaded = true; + + metadataLoaded(); + }); + + jobPtr = netJob; + jobPtr->start(); +} + +// expects current.metadataLoaded to be true +void TechnicPage::metadataLoaded() +{ + QString text = ""; + QString name = current.name; + + if (current.websiteUrl.isEmpty()) + text = name.toHtmlEscaped(); + else + text = "" + name.toHtmlEscaped() + ""; + + if (!current.author.isEmpty()) { + text += "
" + tr(" by ") + current.author.toHtmlEscaped(); + } + + text += "

"; + + ui->packDescription->setHtml(text + current.description); + + // Strip trailing forward-slashes from Solder URL's + if (current.isSolder) { + while (current.url.endsWith('/')) current.url.chop(1); + } + + // Display versions from Solder + if (!current.isSolder) { + // If the pack isn't a Solder pack, it only has the single version + ui->versionSelectionBox->addItem(current.currentVersion); + } + else if (current.versionsLoaded) { + // reverse foreach, so that the newest versions are first + for (auto i = current.versions.size(); i--;) { + ui->versionSelectionBox->addItem(current.versions.at(i)); + } + ui->versionSelectionBox->setCurrentText(current.recommended); + } + else { + // For now, until the versions are pulled from the Solder instance, display the current + // version so we can display something quicker + ui->versionSelectionBox->addItem(current.currentVersion); + + auto* netJob = new NetJob(QString("Technic::SolderMeta(%1)").arg(current.name), APPLICATION->network()); + auto url = QString("%1/modpack/%2").arg(current.url, current.slug); + netJob->addNetAction(Net::Download::makeByteArray(QUrl(url), &response)); + + QObject::connect(netJob, &NetJob::succeeded, this, &TechnicPage::onSolderLoaded); + + jobPtr = netJob; + jobPtr->start(); + } + + selectVersion(); +} + +void TechnicPage::selectVersion() { + if (!isOpened) { + return; + } + if (current.broken) { + dialog->setSuggestedPack(); + return; + } + + if (!current.isSolder) + { + dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SingleZipPackInstallTask(current.url, current.minecraftVersion)); + } + else + { + dialog->setSuggestedPack(current.name + " " + selectedVersion, new Technic::SolderPackInstallTask(APPLICATION->network(), current.url + "/modpack/" + current.slug, selectedVersion, current.minecraftVersion)); + } +} + +void TechnicPage::onSolderLoaded() { + jobPtr.reset(); + + auto fallback = [this]() { + current.versionsLoaded = true; + + current.versions.clear(); + current.versions.append(current.currentVersion); + }; + + current.versions.clear(); + + QJsonParseError parse_error {}; + auto doc = QJsonDocument::fromJson(response, &parse_error); + if (parse_error.error != QJsonParseError::NoError) { + qWarning() << "Error while parsing JSON response from Solder at " << parse_error.offset << " reason: " << parse_error.errorString(); + qWarning() << response; + fallback(); + return; + } + auto obj = doc.object(); + + TechnicSolder::Pack pack; + try { + TechnicSolder::loadPack(pack, obj); + } + catch (const JSONValidationError &err) { + qCritical() << "Couldn't parse Solder pack metadata:" << err.cause(); + fallback(); + return; + } + + current.versionsLoaded = true; + current.recommended = pack.recommended; + current.versions << pack.builds; + + // Finally, let's reload :) + ui->versionSelectionBox->clear(); + metadataLoaded(); +} + +void TechnicPage::onVersionSelectionChanged(QString data) { + if (data.isNull() || data.isEmpty()) { + selectedVersion = ""; + return; + } + + selectedVersion = data; + selectVersion(); +} diff --git a/ultimmc/launcher/ui/pages/modplatform/technic/TechnicPage.h b/ultimmc/launcher/ui/pages/modplatform/technic/TechnicPage.h new file mode 100644 index 0000000..920ca00 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/technic/TechnicPage.h @@ -0,0 +1,88 @@ +/* Copyright 2013-2021 MultiMC Contributors + * Copyright 2021-2022 Jamie Mansfield + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +#include "ui/pages/BasePage.h" +#include +#include "net/NetJob.h" +#include "tasks/Task.h" +#include "TechnicData.h" + +namespace Ui +{ +class TechnicPage; +} + +class NewInstanceDialog; + +namespace Technic { + class ListModel; +} + +class TechnicPage : public QWidget, public BasePage +{ + Q_OBJECT + +public: + explicit TechnicPage(NewInstanceDialog* dialog, QWidget *parent = 0); + virtual ~TechnicPage(); + virtual QString displayName() const override + { + return tr("Technic"); + } + virtual QIcon icon() const override + { + return APPLICATION->getThemedIcon("technic"); + } + virtual QString id() const override + { + return "technic"; + } + virtual QString helpPage() const override + { + return "Technic-platform"; + } + virtual bool shouldDisplay() const override; + + void openedImpl() override; + + bool eventFilter(QObject* watched, QEvent* event) override; + +private: + void suggestCurrent(); + void metadataLoaded(); + void selectVersion(); + +private slots: + void triggerSearch(); + void onSelectionChanged(QModelIndex first, QModelIndex second); + void onSolderLoaded(); + void onVersionSelectionChanged(QString data); + +private: + Ui::TechnicPage *ui = nullptr; + NewInstanceDialog* dialog = nullptr; + Technic::ListModel* model = nullptr; + + Technic::Modpack current; + QString selectedVersion; + + NetJob::Ptr jobPtr; + QByteArray response; +}; diff --git a/ultimmc/launcher/ui/pages/modplatform/technic/TechnicPage.ui b/ultimmc/launcher/ui/pages/modplatform/technic/TechnicPage.ui new file mode 100644 index 0000000..aa2df37 --- /dev/null +++ b/ultimmc/launcher/ui/pages/modplatform/technic/TechnicPage.ui @@ -0,0 +1,85 @@ + + + TechnicPage + + + + 0 + 0 + 546 + 405 + + + + + + + + + + + + Version selected: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + QSizePolicy::Preferred + + + + 1 + 1 + + + + + + + + + + + + true + + + + 48 + 48 + + + + + + + + + + + + + Search and filter ... + + + + + + + Search + + + + + + + + diff --git a/ultimmc/launcher/ui/setupwizard/AnalyticsWizardPage.cpp b/ultimmc/launcher/ui/setupwizard/AnalyticsWizardPage.cpp new file mode 100644 index 0000000..3db2f6d --- /dev/null +++ b/ultimmc/launcher/ui/setupwizard/AnalyticsWizardPage.cpp @@ -0,0 +1,63 @@ +#include "AnalyticsWizardPage.h" +#include + +#include +#include +#include + +#include +#include + +AnalyticsWizardPage::AnalyticsWizardPage(QWidget *parent) + : BaseWizardPage(parent) +{ + setObjectName(QStringLiteral("analyticsPage")); + verticalLayout_3 = new QVBoxLayout(this); + verticalLayout_3->setObjectName(QStringLiteral("verticalLayout_3")); + textBrowser = new QTextBrowser(this); + textBrowser->setObjectName(QStringLiteral("textBrowser")); + textBrowser->setAcceptRichText(false); + textBrowser->setOpenExternalLinks(true); + verticalLayout_3->addWidget(textBrowser); + + checkBox = new QCheckBox(this); + checkBox->setObjectName(QStringLiteral("checkBox")); + checkBox->setChecked(true); + verticalLayout_3->addWidget(checkBox); + retranslate(); +} + +AnalyticsWizardPage::~AnalyticsWizardPage() +{ +} + +bool AnalyticsWizardPage::validatePage() +{ + auto settings = APPLICATION->settings(); + auto analytics = APPLICATION->analytics(); + auto status = checkBox->isChecked(); + settings->set("AnalyticsSeen", analytics->version()); + settings->set("Analytics", status); + return true; +} + +void AnalyticsWizardPage::retranslate() +{ + setTitle(tr("Analytics")); + setSubTitle(tr("We track some anonymous statistics about users.")); + textBrowser->setHtml(tr( + "" + "

The launcher sends anonymous usage statistics on every start of the application. This helps us decide what platforms and issues to focus on.

" + "

The data is processed by Google Analytics, see their article on the " + "matter.

" + "

The following data is collected:

" + "
  • A random unique ID of the installation.
    It is stored in the application settings file.
  • " + "
  • Anonymized (partial) IP address.
  • " + "
  • Launcher version.
  • " + "
  • Operating system name, version and architecture.
  • " + "
  • CPU architecture (kernel architecture on linux).
  • " + "
  • Size of system memory.
  • " + "
  • Java version, architecture and memory settings.
" + "

If we change the tracked information, you will see this page again.

")); + checkBox->setText(tr("Enable Analytics")); +} diff --git a/ultimmc/launcher/ui/setupwizard/AnalyticsWizardPage.h b/ultimmc/launcher/ui/setupwizard/AnalyticsWizardPage.h new file mode 100644 index 0000000..c451db2 --- /dev/null +++ b/ultimmc/launcher/ui/setupwizard/AnalyticsWizardPage.h @@ -0,0 +1,25 @@ +#pragma once + +#include "BaseWizardPage.h" + +class QVBoxLayout; +class QTextBrowser; +class QCheckBox; + +class AnalyticsWizardPage : public BaseWizardPage +{ + Q_OBJECT +public: + explicit AnalyticsWizardPage(QWidget *parent = Q_NULLPTR); + virtual ~AnalyticsWizardPage(); + + bool validatePage() override; + +protected: + void retranslate() override; + +private: + QVBoxLayout *verticalLayout_3 = nullptr; + QTextBrowser *textBrowser = nullptr; + QCheckBox *checkBox = nullptr; +}; \ No newline at end of file diff --git a/ultimmc/launcher/ui/setupwizard/BaseWizardPage.h b/ultimmc/launcher/ui/setupwizard/BaseWizardPage.h new file mode 100644 index 0000000..72dbecf --- /dev/null +++ b/ultimmc/launcher/ui/setupwizard/BaseWizardPage.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +class BaseWizardPage : public QWizardPage +{ +public: + explicit BaseWizardPage(QWidget *parent = Q_NULLPTR) + : QWizardPage(parent) + { + } + virtual ~BaseWizardPage() {}; + + virtual bool wantsRefreshButton() + { + return false; + } + virtual void refresh() + { + } + +protected: + virtual void retranslate() = 0; + void changeEvent(QEvent * event) override + { + if (event->type() == QEvent::LanguageChange) + { + retranslate(); + } + QWizardPage::changeEvent(event); + } +}; diff --git a/ultimmc/launcher/ui/setupwizard/JavaWizardPage.cpp b/ultimmc/launcher/ui/setupwizard/JavaWizardPage.cpp new file mode 100644 index 0000000..63b3d48 --- /dev/null +++ b/ultimmc/launcher/ui/setupwizard/JavaWizardPage.cpp @@ -0,0 +1,98 @@ +#include "JavaWizardPage.h" +#include "Application.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "FileSystem.h" +#include "java/JavaInstall.h" +#include "java/JavaUtils.h" +#include "JavaCommon.h" + +#include "ui/widgets/VersionSelectWidget.h" +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/widgets/JavaSettingsWidget.h" + + +JavaWizardPage::JavaWizardPage(QWidget *parent) + :BaseWizardPage(parent) +{ + setupUi(); +} + +void JavaWizardPage::setupUi() +{ + setObjectName(QStringLiteral("javaPage")); + QVBoxLayout * layout = new QVBoxLayout(this); + + m_java_widget = new JavaSettingsWidget(this); + layout->addWidget(m_java_widget); + setLayout(layout); + + retranslate(); +} + +void JavaWizardPage::refresh() +{ + m_java_widget->refresh(); +} + +void JavaWizardPage::initializePage() +{ + m_java_widget->initialize(); +} + +bool JavaWizardPage::wantsRefreshButton() +{ + return true; +} + +bool JavaWizardPage::validatePage() +{ + auto settings = APPLICATION->settings(); + auto result = m_java_widget->validate(); + switch(result) + { + default: + case JavaSettingsWidget::ValidationStatus::Bad: + { + return false; + } + case JavaSettingsWidget::ValidationStatus::AllOK: + { + settings->set("JavaPath", m_java_widget->javaPath()); + } + case JavaSettingsWidget::ValidationStatus::JavaBad: + { + // Memory + auto s = APPLICATION->settings(); + s->set("MinMemAlloc", m_java_widget->minHeapSize()); + s->set("MaxMemAlloc", m_java_widget->maxHeapSize()); + if (m_java_widget->permGenEnabled()) + { + s->set("PermGen", m_java_widget->permGenSize()); + } + else + { + s->reset("PermGen"); + } + return true; + } + } +} + +void JavaWizardPage::retranslate() +{ + setTitle(tr("Java")); + setSubTitle(tr("You do not have a working Java set up yet or it went missing.\n" + "Please select one of the following or browse for a java executable.")); + m_java_widget->retranslate(); +} diff --git a/ultimmc/launcher/ui/setupwizard/JavaWizardPage.h b/ultimmc/launcher/ui/setupwizard/JavaWizardPage.h new file mode 100644 index 0000000..0d74903 --- /dev/null +++ b/ultimmc/launcher/ui/setupwizard/JavaWizardPage.h @@ -0,0 +1,29 @@ +#pragma once + +#include "BaseWizardPage.h" + +class JavaSettingsWidget; + +class JavaWizardPage : public BaseWizardPage +{ + Q_OBJECT +public: + explicit JavaWizardPage(QWidget *parent = Q_NULLPTR); + + virtual ~JavaWizardPage() + { + }; + + bool wantsRefreshButton() override; + void refresh() override; + void initializePage() override; + bool validatePage() override; + +protected: /* methods */ + void setupUi(); + void retranslate() override; + +private: /* data */ + JavaSettingsWidget *m_java_widget = nullptr; +}; + diff --git a/ultimmc/launcher/ui/setupwizard/LanguageWizardPage.cpp b/ultimmc/launcher/ui/setupwizard/LanguageWizardPage.cpp new file mode 100644 index 0000000..072df10 --- /dev/null +++ b/ultimmc/launcher/ui/setupwizard/LanguageWizardPage.cpp @@ -0,0 +1,49 @@ +#include "LanguageWizardPage.h" +#include +#include + +#include "ui/widgets/LanguageSelectionWidget.h" +#include +#include + +LanguageWizardPage::LanguageWizardPage(QWidget *parent) + : BaseWizardPage(parent) +{ + setObjectName(QStringLiteral("languagePage")); + auto layout = new QVBoxLayout(this); + mainWidget = new LanguageSelectionWidget(this); + layout->setContentsMargins(0,0,0,0); + layout->addWidget(mainWidget); + + retranslate(); +} + +LanguageWizardPage::~LanguageWizardPage() +{ +} + +bool LanguageWizardPage::wantsRefreshButton() +{ + return true; +} + +void LanguageWizardPage::refresh() +{ + auto translations = APPLICATION->translations(); + translations->downloadIndex(); +} + +bool LanguageWizardPage::validatePage() +{ + auto settings = APPLICATION->settings(); + QString key = mainWidget->getSelectedLanguageKey(); + settings->set("Language", key); + return true; +} + +void LanguageWizardPage::retranslate() +{ + setTitle(tr("Language")); + setSubTitle(tr("Select the language to use in %1").arg(BuildConfig.LAUNCHER_NAME)); + mainWidget->retranslate(); +} diff --git a/ultimmc/launcher/ui/setupwizard/LanguageWizardPage.h b/ultimmc/launcher/ui/setupwizard/LanguageWizardPage.h new file mode 100644 index 0000000..45a0e5c --- /dev/null +++ b/ultimmc/launcher/ui/setupwizard/LanguageWizardPage.h @@ -0,0 +1,26 @@ +#pragma once + +#include "BaseWizardPage.h" + +class LanguageSelectionWidget; + +class LanguageWizardPage : public BaseWizardPage +{ + Q_OBJECT +public: + explicit LanguageWizardPage(QWidget *parent = Q_NULLPTR); + + virtual ~LanguageWizardPage(); + + bool wantsRefreshButton() override; + + void refresh() override; + + bool validatePage() override; + +protected: + void retranslate() override; + +private: + LanguageSelectionWidget *mainWidget = nullptr; +}; diff --git a/ultimmc/launcher/ui/setupwizard/SetupWizard.cpp b/ultimmc/launcher/ui/setupwizard/SetupWizard.cpp new file mode 100644 index 0000000..5af5ba9 --- /dev/null +++ b/ultimmc/launcher/ui/setupwizard/SetupWizard.cpp @@ -0,0 +1,89 @@ +#include "SetupWizard.h" + +#include "LanguageWizardPage.h" +#include "JavaWizardPage.h" +#include "AnalyticsWizardPage.h" + +#include "translations/TranslationsModel.h" +#include +#include +#include + +#include +#include + +SetupWizard::SetupWizard(QWidget *parent) : QWizard(parent) +{ + setObjectName(QStringLiteral("SetupWizard")); + resize(615, 659); + // make it ugly everywhere to avoid variability in theming + setWizardStyle(QWizard::ClassicStyle); + setOptions(QWizard::NoCancelButton | QWizard::IndependentPages | QWizard::HaveCustomButton1); + + retranslate(); + + connect(this, &QWizard::currentIdChanged, this, &SetupWizard::pageChanged); +} + +void SetupWizard::retranslate() +{ + setButtonText(QWizard::NextButton, tr("&Next >")); + setButtonText(QWizard::BackButton, tr("< &Back")); + setButtonText(QWizard::FinishButton, tr("&Finish")); + setButtonText(QWizard::CustomButton1, tr("&Refresh")); + setWindowTitle(tr("%1 Quick Setup").arg(BuildConfig.LAUNCHER_NAME)); +} + +BaseWizardPage * SetupWizard::getBasePage(int id) +{ + if(id == -1) + return nullptr; + auto pagePtr = page(id); + if(!pagePtr) + return nullptr; + return dynamic_cast(pagePtr); +} + +BaseWizardPage * SetupWizard::getCurrentBasePage() +{ + return getBasePage(currentId()); +} + +void SetupWizard::pageChanged(int id) +{ + auto basePagePtr = getBasePage(id); + if(!basePagePtr) + { + return; + } + if(basePagePtr->wantsRefreshButton()) + { + setButtonLayout({QWizard::CustomButton1, QWizard::Stretch, QWizard::BackButton, QWizard::NextButton, QWizard::FinishButton}); + auto customButton = button(QWizard::CustomButton1); + connect(customButton, &QAbstractButton::pressed, [&](){ + auto basePagePtr = getCurrentBasePage(); + if(basePagePtr) + { + basePagePtr->refresh(); + } + }); + } + else + { + setButtonLayout({QWizard::Stretch, QWizard::BackButton, QWizard::NextButton, QWizard::FinishButton}); + } +} + + +void SetupWizard::changeEvent(QEvent *event) +{ + if (event->type() == QEvent::LanguageChange) + { + retranslate(); + } + QWizard::changeEvent(event); +} + +SetupWizard::~SetupWizard() +{ +} diff --git a/ultimmc/launcher/ui/setupwizard/SetupWizard.h b/ultimmc/launcher/ui/setupwizard/SetupWizard.h new file mode 100644 index 0000000..9b8adb4 --- /dev/null +++ b/ultimmc/launcher/ui/setupwizard/SetupWizard.h @@ -0,0 +1,45 @@ +/* Copyright 2017-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace Ui +{ +class SetupWizard; +} + +class BaseWizardPage; + +class SetupWizard : public QWizard +{ + Q_OBJECT + +public: /* con/destructors */ + explicit SetupWizard(QWidget *parent = 0); + virtual ~SetupWizard(); + + void changeEvent(QEvent * event) override; + BaseWizardPage *getBasePage(int id); + BaseWizardPage *getCurrentBasePage(); + +private slots: + void pageChanged(int id); + +private: /* methods */ + void retranslate(); +}; + diff --git a/ultimmc/launcher/ui/themes/BrightTheme.cpp b/ultimmc/launcher/ui/themes/BrightTheme.cpp new file mode 100644 index 0000000..b9188bd --- /dev/null +++ b/ultimmc/launcher/ui/themes/BrightTheme.cpp @@ -0,0 +1,56 @@ +#include "BrightTheme.h" + +QString BrightTheme::id() +{ + return "bright"; +} + +QString BrightTheme::name() +{ + return QObject::tr("Bright"); +} + +bool BrightTheme::hasColorScheme() +{ + return true; +} + +QPalette BrightTheme::colorScheme() +{ + QPalette brightPalette; + brightPalette.setColor(QPalette::Window, QColor(239,240,241)); + brightPalette.setColor(QPalette::WindowText, QColor(49,54,59)); + brightPalette.setColor(QPalette::Base, QColor(252,252,252)); + brightPalette.setColor(QPalette::AlternateBase, QColor(239,240,241)); + brightPalette.setColor(QPalette::ToolTipBase, QColor(49,54,59)); + brightPalette.setColor(QPalette::ToolTipText, QColor(239,240,241)); + brightPalette.setColor(QPalette::Text, QColor(49,54,59)); + brightPalette.setColor(QPalette::Button, QColor(239,240,241)); + brightPalette.setColor(QPalette::ButtonText, QColor(49,54,59)); + brightPalette.setColor(QPalette::BrightText, Qt::red); + brightPalette.setColor(QPalette::Link, QColor(41, 128, 185)); + brightPalette.setColor(QPalette::Highlight, QColor(61, 174, 233)); + brightPalette.setColor(QPalette::HighlightedText, QColor(239,240,241)); + return fadeInactive(brightPalette, fadeAmount(), fadeColor()); +} + +double BrightTheme::fadeAmount() +{ + return 0.5; +} + +QColor BrightTheme::fadeColor() +{ + return QColor(239,240,241); +} + +bool BrightTheme::hasStyleSheet() +{ + return false; +} + +QString BrightTheme::appStyleSheet() +{ + return QString(); +} + diff --git a/ultimmc/launcher/ui/themes/BrightTheme.h b/ultimmc/launcher/ui/themes/BrightTheme.h new file mode 100644 index 0000000..c61f52d --- /dev/null +++ b/ultimmc/launcher/ui/themes/BrightTheme.h @@ -0,0 +1,19 @@ +#pragma once + +#include "FusionTheme.h" + +class BrightTheme: public FusionTheme +{ +public: + virtual ~BrightTheme() {} + + QString id() override; + QString name() override; + bool hasStyleSheet() override; + QString appStyleSheet() override; + bool hasColorScheme() override; + QPalette colorScheme() override; + double fadeAmount() override; + QColor fadeColor() override; +}; + diff --git a/ultimmc/launcher/ui/themes/CustomTheme.cpp b/ultimmc/launcher/ui/themes/CustomTheme.cpp new file mode 100644 index 0000000..3e3e27d --- /dev/null +++ b/ultimmc/launcher/ui/themes/CustomTheme.cpp @@ -0,0 +1,244 @@ +#include "CustomTheme.h" +#include +#include +#include + +const char * themeFile = "theme.json"; +const char * styleFile = "themeStyle.css"; + +static bool readThemeJson(const QString &path, QPalette &palette, double &fadeAmount, QColor &fadeColor, QString &name, QString &widgets) +{ + QFileInfo pathInfo(path); + if(pathInfo.exists() && pathInfo.isFile()) + { + try + { + auto doc = Json::requireDocument(path, "Theme JSON file"); + const QJsonObject root = doc.object(); + name = Json::requireString(root, "name", "Theme name"); + widgets = Json::requireString(root, "widgets", "Qt widget theme"); + auto colorsRoot = Json::requireObject(root, "colors", "colors object"); + auto readColor = [&](QString colorName) -> QColor + { + auto colorValue = Json::ensureString(colorsRoot, colorName, QString()); + if(!colorValue.isEmpty()) + { + QColor color(colorValue); + if(!color.isValid()) + { + qWarning() << "Color value" << colorValue << "for" << colorName << "was not recognized."; + return QColor(); + } + return color; + } + return QColor(); + }; + auto readAndSetColor = [&](QPalette::ColorRole role, QString colorName) + { + auto color = readColor(colorName); + if(color.isValid()) + { + palette.setColor(role, color); + } + else + { + qDebug() << "Color value for" << colorName << "was not present."; + } + }; + + // palette + readAndSetColor(QPalette::Window, "Window"); + readAndSetColor(QPalette::WindowText, "WindowText"); + readAndSetColor(QPalette::Base, "Base"); + readAndSetColor(QPalette::AlternateBase, "AlternateBase"); + readAndSetColor(QPalette::ToolTipBase, "ToolTipBase"); + readAndSetColor(QPalette::ToolTipText, "ToolTipText"); + readAndSetColor(QPalette::Text, "Text"); + readAndSetColor(QPalette::Button, "Button"); + readAndSetColor(QPalette::ButtonText, "ButtonText"); + readAndSetColor(QPalette::BrightText, "BrightText"); + readAndSetColor(QPalette::Link, "Link"); + readAndSetColor(QPalette::Highlight, "Highlight"); + readAndSetColor(QPalette::HighlightedText, "HighlightedText"); + + //fade + fadeColor = readColor("fadeColor"); + fadeAmount = Json::ensureDouble(colorsRoot, "fadeAmount", 0.5, "fade amount"); + + } + catch (const Exception &e) + { + qWarning() << "Couldn't load theme json: " << e.cause(); + return false; + } + } + else + { + qDebug() << "No theme json present."; + return false; + } + return true; +} + +static bool writeThemeJson(const QString &path, const QPalette &palette, double fadeAmount, QColor fadeColor, QString name, QString widgets) +{ + QJsonObject rootObj; + rootObj.insert("name", name); + rootObj.insert("widgets", widgets); + + QJsonObject colorsObj; + auto insertColor = [&](QPalette::ColorRole role, QString colorName) + { + colorsObj.insert(colorName, palette.color(role).name()); + }; + + // palette + insertColor(QPalette::Window, "Window"); + insertColor(QPalette::WindowText, "WindowText"); + insertColor(QPalette::Base, "Base"); + insertColor(QPalette::AlternateBase, "AlternateBase"); + insertColor(QPalette::ToolTipBase, "ToolTipBase"); + insertColor(QPalette::ToolTipText, "ToolTipText"); + insertColor(QPalette::Text, "Text"); + insertColor(QPalette::Button, "Button"); + insertColor(QPalette::ButtonText, "ButtonText"); + insertColor(QPalette::BrightText, "BrightText"); + insertColor(QPalette::Link, "Link"); + insertColor(QPalette::Highlight, "Highlight"); + insertColor(QPalette::HighlightedText, "HighlightedText"); + + // fade + colorsObj.insert("fadeColor", fadeColor.name()); + colorsObj.insert("fadeAmount", fadeAmount); + + rootObj.insert("colors", colorsObj); + try + { + Json::write(rootObj, path); + return true; + } + catch (const Exception &e) + { + qWarning() << "Failed to write theme json to" << path; + return false; + } +} + +CustomTheme::CustomTheme(ITheme* baseTheme, QString folder) +{ + m_id = folder; + QString path = FS::PathCombine("themes", m_id); + QString pathResources = FS::PathCombine("themes", m_id, "resources"); + + qDebug() << "Loading theme" << m_id; + + if(!FS::ensureFolderPathExists(path) || !FS::ensureFolderPathExists(pathResources)) + { + qWarning() << "couldn't create folder for theme!"; + m_palette = baseTheme->colorScheme(); + m_styleSheet = baseTheme->appStyleSheet(); + return; + } + + auto themeFilePath = FS::PathCombine(path, themeFile); + + m_palette = baseTheme->colorScheme(); + if (!readThemeJson(themeFilePath, m_palette, m_fadeAmount, m_fadeColor, m_name, m_widgets)) + { + m_name = "Custom"; + m_palette = baseTheme->colorScheme(); + m_fadeColor = baseTheme->fadeColor(); + m_fadeAmount = baseTheme->fadeAmount(); + m_widgets = baseTheme->qtTheme(); + + QFileInfo info(themeFilePath); + if(!info.exists()) + { + writeThemeJson(themeFilePath, m_palette, m_fadeAmount, m_fadeColor, "Custom", m_widgets); + } + } + else + { + m_palette = fadeInactive(m_palette, m_fadeAmount, m_fadeColor); + } + + auto cssFilePath = FS::PathCombine(path, styleFile); + QFileInfo info (cssFilePath); + if(info.isFile()) + { + try + { + // TODO: validate css? + m_styleSheet = QString::fromUtf8(FS::read(cssFilePath)); + } + catch (const Exception &e) + { + qWarning() << "Couldn't load css:" << e.cause() << "from" << cssFilePath; + m_styleSheet = baseTheme->appStyleSheet(); + } + } + else + { + qDebug() << "No theme css present."; + m_styleSheet = baseTheme->appStyleSheet(); + try + { + FS::write(cssFilePath, m_styleSheet.toUtf8()); + } + catch (const Exception &e) + { + qWarning() << "Couldn't write css:" << e.cause() << "to" << cssFilePath; + } + } +} + +QStringList CustomTheme::searchPaths() +{ + return { FS::PathCombine("themes", m_id, "resources") }; +} + + +QString CustomTheme::id() +{ + return m_id; +} + +QString CustomTheme::name() +{ + return m_name; +} + +bool CustomTheme::hasColorScheme() +{ + return true; +} + +QPalette CustomTheme::colorScheme() +{ + return m_palette; +} + +bool CustomTheme::hasStyleSheet() +{ + return true; +} + +QString CustomTheme::appStyleSheet() +{ + return m_styleSheet; +} + +double CustomTheme::fadeAmount() +{ + return m_fadeAmount; +} + +QColor CustomTheme::fadeColor() +{ + return m_fadeColor; +} + +QString CustomTheme::qtTheme() +{ + return m_widgets; +} diff --git a/ultimmc/launcher/ui/themes/CustomTheme.h b/ultimmc/launcher/ui/themes/CustomTheme.h new file mode 100644 index 0000000..d216895 --- /dev/null +++ b/ultimmc/launcher/ui/themes/CustomTheme.h @@ -0,0 +1,31 @@ +#pragma once + +#include "ITheme.h" + +class CustomTheme: public ITheme +{ +public: + CustomTheme(ITheme * baseTheme, QString folder); + virtual ~CustomTheme() {} + + QString id() override; + QString name() override; + bool hasStyleSheet() override; + QString appStyleSheet() override; + bool hasColorScheme() override; + QPalette colorScheme() override; + double fadeAmount() override; + QColor fadeColor() override; + QString qtTheme() override; + QStringList searchPaths() override; + +private: /* data */ + QPalette m_palette; + QColor m_fadeColor; + double m_fadeAmount; + QString m_styleSheet; + QString m_name; + QString m_id; + QString m_widgets; +}; + diff --git a/ultimmc/launcher/ui/themes/DarkTheme.cpp b/ultimmc/launcher/ui/themes/DarkTheme.cpp new file mode 100644 index 0000000..31ecd55 --- /dev/null +++ b/ultimmc/launcher/ui/themes/DarkTheme.cpp @@ -0,0 +1,55 @@ +#include "DarkTheme.h" + +QString DarkTheme::id() +{ + return "dark"; +} + +QString DarkTheme::name() +{ + return QObject::tr("Dark"); +} + +bool DarkTheme::hasColorScheme() +{ + return true; +} + +QPalette DarkTheme::colorScheme() +{ + QPalette darkPalette; + darkPalette.setColor(QPalette::Window, QColor(49,54,59)); + darkPalette.setColor(QPalette::WindowText, Qt::white); + darkPalette.setColor(QPalette::Base, QColor(35,38,41)); + darkPalette.setColor(QPalette::AlternateBase, QColor(49,54,59)); + darkPalette.setColor(QPalette::ToolTipBase, Qt::white); + darkPalette.setColor(QPalette::ToolTipText, Qt::white); + darkPalette.setColor(QPalette::Text, Qt::white); + darkPalette.setColor(QPalette::Button, QColor(49,54,59)); + darkPalette.setColor(QPalette::ButtonText, Qt::white); + darkPalette.setColor(QPalette::BrightText, Qt::red); + darkPalette.setColor(QPalette::Link, QColor(42, 130, 218)); + darkPalette.setColor(QPalette::Highlight, QColor(42, 130, 218)); + darkPalette.setColor(QPalette::HighlightedText, Qt::black); + return fadeInactive(darkPalette, fadeAmount(), fadeColor()); +} + +double DarkTheme::fadeAmount() +{ + return 0.5; +} + +QColor DarkTheme::fadeColor() +{ + return QColor(49,54,59); +} + +bool DarkTheme::hasStyleSheet() +{ + return true; +} + +QString DarkTheme::appStyleSheet() +{ + return "QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }"; +} diff --git a/ultimmc/launcher/ui/themes/DarkTheme.h b/ultimmc/launcher/ui/themes/DarkTheme.h new file mode 100644 index 0000000..9bd2f34 --- /dev/null +++ b/ultimmc/launcher/ui/themes/DarkTheme.h @@ -0,0 +1,18 @@ +#pragma once + +#include "FusionTheme.h" + +class DarkTheme: public FusionTheme +{ +public: + virtual ~DarkTheme() {} + + QString id() override; + QString name() override; + bool hasStyleSheet() override; + QString appStyleSheet() override; + bool hasColorScheme() override; + QPalette colorScheme() override; + double fadeAmount() override; + QColor fadeColor() override; +}; diff --git a/ultimmc/launcher/ui/themes/FusionTheme.cpp b/ultimmc/launcher/ui/themes/FusionTheme.cpp new file mode 100644 index 0000000..cf3286b --- /dev/null +++ b/ultimmc/launcher/ui/themes/FusionTheme.cpp @@ -0,0 +1,6 @@ +#include "FusionTheme.h" + +QString FusionTheme::qtTheme() +{ + return "Fusion"; +} diff --git a/ultimmc/launcher/ui/themes/FusionTheme.h b/ultimmc/launcher/ui/themes/FusionTheme.h new file mode 100644 index 0000000..ee34245 --- /dev/null +++ b/ultimmc/launcher/ui/themes/FusionTheme.h @@ -0,0 +1,11 @@ +#pragma once + +#include "ITheme.h" + +class FusionTheme: public ITheme +{ +public: + virtual ~FusionTheme() {} + + QString qtTheme() override; +}; diff --git a/ultimmc/launcher/ui/themes/ITheme.cpp b/ultimmc/launcher/ui/themes/ITheme.cpp new file mode 100644 index 0000000..7247b44 --- /dev/null +++ b/ultimmc/launcher/ui/themes/ITheme.cpp @@ -0,0 +1,47 @@ +#include "ITheme.h" +#include "rainbow.h" +#include +#include +#include "Application.h" + +void ITheme::apply(bool) +{ + QApplication::setStyle(QStyleFactory::create(qtTheme())); + if(hasColorScheme()) + { + QApplication::setPalette(colorScheme()); + } + if(hasStyleSheet()) + { + APPLICATION->setStyleSheet(appStyleSheet()); + } + else + { + APPLICATION->setStyleSheet(QString()); + } + QDir::setSearchPaths("theme", searchPaths()); +} + +QPalette ITheme::fadeInactive(QPalette in, qreal bias, QColor color) +{ + auto blend = [&in, bias, color](QPalette::ColorRole role) + { + QColor from = in.color(QPalette::Active, role); + QColor blended = Rainbow::mix(from, color, bias); + in.setColor(QPalette::Disabled, role, blended); + }; + blend(QPalette::Window); + blend(QPalette::WindowText); + blend(QPalette::Base); + blend(QPalette::AlternateBase); + blend(QPalette::ToolTipBase); + blend(QPalette::ToolTipText); + blend(QPalette::Text); + blend(QPalette::Button); + blend(QPalette::ButtonText); + blend(QPalette::BrightText); + blend(QPalette::Link); + blend(QPalette::Highlight); + blend(QPalette::HighlightedText); + return in; +} diff --git a/ultimmc/launcher/ui/themes/ITheme.h b/ultimmc/launcher/ui/themes/ITheme.h new file mode 100644 index 0000000..c2347cf --- /dev/null +++ b/ultimmc/launcher/ui/themes/ITheme.h @@ -0,0 +1,27 @@ +#pragma once +#include +#include + +class QStyle; + +class ITheme +{ +public: + virtual ~ITheme() {} + virtual void apply(bool initial); + virtual QString id() = 0; + virtual QString name() = 0; + virtual bool hasStyleSheet() = 0; + virtual QString appStyleSheet() = 0; + virtual QString qtTheme() = 0; + virtual bool hasColorScheme() = 0; + virtual QPalette colorScheme() = 0; + virtual QColor fadeColor() = 0; + virtual double fadeAmount() = 0; + virtual QStringList searchPaths() + { + return {}; + } + + static QPalette fadeInactive(QPalette in, qreal bias, QColor color); +}; diff --git a/ultimmc/launcher/ui/themes/SystemTheme.cpp b/ultimmc/launcher/ui/themes/SystemTheme.cpp new file mode 100644 index 0000000..49b1afa --- /dev/null +++ b/ultimmc/launcher/ui/themes/SystemTheme.cpp @@ -0,0 +1,83 @@ +#include "SystemTheme.h" +#include +#include +#include +#include + +SystemTheme::SystemTheme() +{ + qDebug() << "Determining System Theme..."; + const auto & style = QApplication::style(); + systemPalette = style->standardPalette(); + QString lowerThemeName = style->objectName(); + qDebug() << "System theme seems to be:" << lowerThemeName; + QStringList styles = QStyleFactory::keys(); + for(auto &st: styles) + { + qDebug() << "Considering theme from theme factory:" << st.toLower(); + if(st.toLower() == lowerThemeName) + { + systemTheme = st; + qDebug() << "System theme has been determined to be:" << systemTheme; + return; + } + } + // fall back to fusion if we can't find the current theme. + systemTheme = "Fusion"; + qDebug() << "System theme not found, defaulted to Fusion"; +} + +void SystemTheme::apply(bool initial) +{ + // if we are applying the system theme as the first theme, just don't touch anything. it's for the better... + if(initial) + { + return; + } + ITheme::apply(initial); +} + +QString SystemTheme::id() +{ + return "system"; +} + +QString SystemTheme::name() +{ + return QObject::tr("System"); +} + +QString SystemTheme::qtTheme() +{ + return systemTheme; +} + +QPalette SystemTheme::colorScheme() +{ + return systemPalette; +} + +QString SystemTheme::appStyleSheet() +{ + return QString(); +} + +double SystemTheme::fadeAmount() +{ + return 0.5; +} + +QColor SystemTheme::fadeColor() +{ + return QColor(128,128,128); +} + +bool SystemTheme::hasStyleSheet() +{ + return false; +} + +bool SystemTheme::hasColorScheme() +{ + return true; +} diff --git a/ultimmc/launcher/ui/themes/SystemTheme.h b/ultimmc/launcher/ui/themes/SystemTheme.h new file mode 100644 index 0000000..fe45060 --- /dev/null +++ b/ultimmc/launcher/ui/themes/SystemTheme.h @@ -0,0 +1,24 @@ +#pragma once + +#include "ITheme.h" + +class SystemTheme: public ITheme +{ +public: + SystemTheme(); + virtual ~SystemTheme() {} + void apply(bool initial) override; + + QString id() override; + QString name() override; + QString qtTheme() override; + bool hasStyleSheet() override; + QString appStyleSheet() override; + bool hasColorScheme() override; + QPalette colorScheme() override; + double fadeAmount() override; + QColor fadeColor() override; +private: + QPalette systemPalette; + QString systemTheme; +}; diff --git a/ultimmc/launcher/ui/widgets/Common.cpp b/ultimmc/launcher/ui/widgets/Common.cpp new file mode 100644 index 0000000..f72f359 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/Common.cpp @@ -0,0 +1,27 @@ +#include "Common.h" + +// Origin: Qt +QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height, + qreal &widthUsed) +{ + QStringList lines; + height = 0; + widthUsed = 0; + textLayout.beginLayout(); + QString str = textLayout.text(); + while (true) + { + QTextLine line = textLayout.createLine(); + if (!line.isValid()) + break; + if (line.textLength() == 0) + break; + line.setLineWidth(lineWidth); + line.setPosition(QPointF(0, height)); + height += line.height(); + lines.append(str.mid(line.textStart(), line.textLength())); + widthUsed = qMax(widthUsed, line.naturalTextWidth()); + } + textLayout.endLayout(); + return lines; +} diff --git a/ultimmc/launcher/ui/widgets/Common.h b/ultimmc/launcher/ui/widgets/Common.h new file mode 100644 index 0000000..b3fbe1a --- /dev/null +++ b/ultimmc/launcher/ui/widgets/Common.h @@ -0,0 +1,6 @@ +#pragma once +#include +#include + +QStringList viewItemTextLayout(QTextLayout &textLayout, int lineWidth, qreal &height, + qreal &widthUsed); \ No newline at end of file diff --git a/ultimmc/launcher/ui/widgets/CustomCommands.cpp b/ultimmc/launcher/ui/widgets/CustomCommands.cpp new file mode 100644 index 0000000..24bdc07 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/CustomCommands.cpp @@ -0,0 +1,49 @@ +#include "CustomCommands.h" +#include "ui_CustomCommands.h" + +CustomCommands::~CustomCommands() +{ + delete ui; +} + +CustomCommands::CustomCommands(QWidget* parent): + QWidget(parent), + ui(new Ui::CustomCommands) +{ + ui->setupUi(this); +} + +void CustomCommands::initialize(bool checkable, bool checked, const QString& prelaunch, const QString& wrapper, const QString& postexit) +{ + ui->customCommandsGroupBox->setCheckable(checkable); + if(checkable) + { + ui->customCommandsGroupBox->setChecked(checked); + } + ui->preLaunchCmdTextBox->setText(prelaunch); + ui->wrapperCmdTextBox->setText(wrapper); + ui->postExitCmdTextBox->setText(postexit); +} + + +bool CustomCommands::checked() const +{ + if(!ui->customCommandsGroupBox->isCheckable()) + return true; + return ui->customCommandsGroupBox->isChecked(); +} + +QString CustomCommands::prelaunchCommand() const +{ + return ui->preLaunchCmdTextBox->text(); +} + +QString CustomCommands::wrapperCommand() const +{ + return ui->wrapperCmdTextBox->text(); +} + +QString CustomCommands::postexitCommand() const +{ + return ui->postExitCmdTextBox->text(); +} diff --git a/ultimmc/launcher/ui/widgets/CustomCommands.h b/ultimmc/launcher/ui/widgets/CustomCommands.h new file mode 100644 index 0000000..8db991f --- /dev/null +++ b/ultimmc/launcher/ui/widgets/CustomCommands.h @@ -0,0 +1,43 @@ +/* Copyright 2018-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace Ui +{ +class CustomCommands; +} + +class CustomCommands : public QWidget +{ + Q_OBJECT + +public: + explicit CustomCommands(QWidget *parent = 0); + virtual ~CustomCommands(); + void initialize(bool checkable, bool checked, const QString & prelaunch, const QString & wrapper, const QString & postexit); + + bool checked() const; + QString prelaunchCommand() const; + QString wrapperCommand() const; + QString postexitCommand() const; + +private: + Ui::CustomCommands *ui; +}; + + diff --git a/ultimmc/launcher/ui/widgets/CustomCommands.ui b/ultimmc/launcher/ui/widgets/CustomCommands.ui new file mode 100644 index 0000000..21964ad --- /dev/null +++ b/ultimmc/launcher/ui/widgets/CustomCommands.ui @@ -0,0 +1,107 @@ + + + CustomCommands + + + + 0 + 0 + 518 + 646 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + Cus&tom Commands + + + true + + + false + + + + + + Post-exit command: + + + + + + + + + + Pre-launch command: + + + + + + + + + + Wrapper command: + + + + + + + + + + + + + <html><head/><body><p>Pre-launch command runs before the instance launches and post-exit command runs after it exits.</p><p>Both will be run in the launcher's working folder with extra environment variables:</p><ul><li>$INST_NAME - Name of the instance</li><li>$INST_ID - ID of the instance (its folder name)</li><li>$INST_DIR - absolute path of the instance</li><li>$INST_MC_DIR - absolute path of minecraft</li><li>$INST_JAVA - java binary used for launch</li><li>$INST_JAVA_ARGS - command-line parameters used for launch</li></ul><p>Wrapper command allows launching using an extra wrapper program (like 'optirun' on Linux)</p></body></html> + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/ultimmc/launcher/ui/widgets/DropLabel.cpp b/ultimmc/launcher/ui/widgets/DropLabel.cpp new file mode 100644 index 0000000..a900e57 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/DropLabel.cpp @@ -0,0 +1,41 @@ +#include "DropLabel.h" + +#include +#include + +DropLabel::DropLabel(QWidget *parent) : QLabel(parent) +{ + setAcceptDrops(true); +} + +void DropLabel::dragEnterEvent(QDragEnterEvent *event) +{ + event->acceptProposedAction(); +} + +void DropLabel::dragMoveEvent(QDragMoveEvent *event) +{ + event->acceptProposedAction(); +} + +void DropLabel::dragLeaveEvent(QDragLeaveEvent *event) +{ + event->accept(); +} + +void DropLabel::dropEvent(QDropEvent *event) +{ + const QMimeData *mimeData = event->mimeData(); + + if (!mimeData) + { + return; + } + + if (mimeData->hasUrls()) { + auto urls = mimeData->urls(); + emit droppedURLs(urls); + } + + event->acceptProposedAction(); +} diff --git a/ultimmc/launcher/ui/widgets/DropLabel.h b/ultimmc/launcher/ui/widgets/DropLabel.h new file mode 100644 index 0000000..c5ca0bc --- /dev/null +++ b/ultimmc/launcher/ui/widgets/DropLabel.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +class DropLabel : public QLabel +{ + Q_OBJECT + +public: + explicit DropLabel(QWidget *parent = nullptr); + +signals: + void droppedURLs(QList urls); + +protected: + void dropEvent(QDropEvent *event) override; + void dragEnterEvent(QDragEnterEvent *event) override; + void dragMoveEvent(QDragMoveEvent *event) override; + void dragLeaveEvent(QDragLeaveEvent *event) override; +}; diff --git a/ultimmc/launcher/ui/widgets/ErrorFrame.cpp b/ultimmc/launcher/ui/widgets/ErrorFrame.cpp new file mode 100644 index 0000000..b3e4103 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/ErrorFrame.cpp @@ -0,0 +1,134 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "ErrorFrame.h" +#include "ui_ErrorFrame.h" + +#include "ui/dialogs/CustomMessageBox.h" + +void ErrorFrame::clear() +{ + setTitle(QString()); + setDescription(QString()); +} + +ErrorFrame::ErrorFrame(QWidget *parent) : + QFrame(parent), + ui(new Ui::ErrorFrame) +{ + ui->setupUi(this); + ui->label_Description->setHidden(true); + ui->label_Title->setHidden(true); + updateHiddenState(); +} + +ErrorFrame::~ErrorFrame() +{ + delete ui; +} + +void ErrorFrame::updateHiddenState() +{ + if(ui->label_Description->isHidden() && ui->label_Title->isHidden()) + { + setHidden(true); + } + else + { + setHidden(false); + } +} + +void ErrorFrame::setTitle(QString text) +{ + if(text.isEmpty()) + { + ui->label_Title->setHidden(true); + } + else + { + ui->label_Title->setText(text); + ui->label_Title->setHidden(false); + } + updateHiddenState(); +} + +void ErrorFrame::setDescription(QString text) +{ + if(text.isEmpty()) + { + ui->label_Description->setHidden(true); + updateHiddenState(); + return; + } + else + { + ui->label_Description->setHidden(false); + updateHiddenState(); + } + ui->label_Description->setToolTip(""); + QString intermediatetext = text.trimmed(); + bool prev(false); + QChar rem('\n'); + QString finaltext; + finaltext.reserve(intermediatetext.size()); + foreach(const QChar& c, intermediatetext) + { + if(c == rem && prev){ + continue; + } + prev = c == rem; + finaltext += c; + } + QString labeltext; + labeltext.reserve(300); + if(finaltext.length() > 290) + { + ui->label_Description->setOpenExternalLinks(false); + ui->label_Description->setTextFormat(Qt::TextFormat::RichText); + desc = text; + // This allows injecting HTML here. + labeltext.append("" + finaltext.left(287) + "..."); + QObject::connect(ui->label_Description, &QLabel::linkActivated, this, &ErrorFrame::ellipsisHandler); + } + else + { + ui->label_Description->setTextFormat(Qt::TextFormat::PlainText); + labeltext.append(finaltext); + } + ui->label_Description->setText(labeltext); +} + +void ErrorFrame::ellipsisHandler(const QString &link) +{ + if(!currentBox) + { + currentBox = CustomMessageBox::selectable(this, QString(), desc); + connect(currentBox, &QMessageBox::finished, this, &ErrorFrame::boxClosed); + currentBox->show(); + } + else + { + currentBox->setText(desc); + } +} + +void ErrorFrame::boxClosed(int result) +{ + currentBox = nullptr; +} diff --git a/ultimmc/launcher/ui/widgets/ErrorFrame.h b/ultimmc/launcher/ui/widgets/ErrorFrame.h new file mode 100644 index 0000000..d5069a1 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/ErrorFrame.h @@ -0,0 +1,49 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace Ui +{ +class ErrorFrame; +} + +class ErrorFrame : public QFrame +{ + Q_OBJECT + +public: + explicit ErrorFrame(QWidget *parent = 0); + ~ErrorFrame(); + + void setTitle(QString text); + void setDescription(QString text); + + void clear(); + +public slots: + void ellipsisHandler(const QString& link ); + void boxClosed(int result); + +private: + void updateHiddenState(); + +private: + Ui::ErrorFrame *ui; + QString desc; + class QMessageBox * currentBox = nullptr; +}; diff --git a/ultimmc/launcher/ui/widgets/ErrorFrame.ui b/ultimmc/launcher/ui/widgets/ErrorFrame.ui new file mode 100644 index 0000000..0bb5674 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/ErrorFrame.ui @@ -0,0 +1,92 @@ + + + ErrorFrame + + + + 0 + 0 + 527 + 113 + + + + + 0 + 0 + + + + + 16777215 + 120 + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + diff --git a/ultimmc/launcher/ui/widgets/FocusLineEdit.cpp b/ultimmc/launcher/ui/widgets/FocusLineEdit.cpp new file mode 100644 index 0000000..b272100 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/FocusLineEdit.cpp @@ -0,0 +1,25 @@ +#include "FocusLineEdit.h" +#include + +FocusLineEdit::FocusLineEdit(QWidget *parent) : QLineEdit(parent) +{ + _selectOnMousePress = false; +} + +void FocusLineEdit::focusInEvent(QFocusEvent *e) +{ + QLineEdit::focusInEvent(e); + selectAll(); + _selectOnMousePress = true; +} + +void FocusLineEdit::mousePressEvent(QMouseEvent *me) +{ + QLineEdit::mousePressEvent(me); + if (_selectOnMousePress) + { + selectAll(); + _selectOnMousePress = false; + } + qDebug() << selectedText(); +} diff --git a/ultimmc/launcher/ui/widgets/FocusLineEdit.h b/ultimmc/launcher/ui/widgets/FocusLineEdit.h new file mode 100644 index 0000000..71b4f14 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/FocusLineEdit.h @@ -0,0 +1,17 @@ +#include + +class FocusLineEdit : public QLineEdit +{ + Q_OBJECT +public: + FocusLineEdit(QWidget *parent); + virtual ~FocusLineEdit() + { + } + +protected: + void focusInEvent(QFocusEvent *e); + void mousePressEvent(QMouseEvent *me); + + bool _selectOnMousePress; +}; diff --git a/ultimmc/launcher/ui/widgets/IconLabel.cpp b/ultimmc/launcher/ui/widgets/IconLabel.cpp new file mode 100644 index 0000000..bf1c235 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/IconLabel.cpp @@ -0,0 +1,43 @@ +#include "IconLabel.h" + +#include +#include +#include +#include +#include + +IconLabel::IconLabel(QWidget *parent, QIcon icon, QSize size) + : QWidget(parent), m_size(size), m_icon(icon) +{ + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); +} + +QSize IconLabel::sizeHint() const +{ + return m_size; +} + +void IconLabel::setIcon(QIcon icon) +{ + m_icon = icon; + update(); +} + +void IconLabel::paintEvent(QPaintEvent *) +{ + QPainter p(this); + QRect rect = contentsRect(); + int width = rect.width(); + int height = rect.height(); + if(width < height) + { + rect.setHeight(width); + rect.translate(0, (height - width) / 2); + } + else if (width > height) + { + rect.setWidth(height); + rect.translate((width - height) / 2, 0); + } + m_icon.paint(&p, rect); +} diff --git a/ultimmc/launcher/ui/widgets/IconLabel.h b/ultimmc/launcher/ui/widgets/IconLabel.h new file mode 100644 index 0000000..6d212c4 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/IconLabel.h @@ -0,0 +1,26 @@ +#pragma once +#include +#include + +class QStyleOption; + +/** + * This is a trivial widget that paints a QIcon of the specified size. + */ +class IconLabel : public QWidget +{ + Q_OBJECT + +public: + /// Create a line separator. orientation is the orientation of the line. + explicit IconLabel(QWidget *parent, QIcon icon, QSize size); + + virtual QSize sizeHint() const; + virtual void paintEvent(QPaintEvent *); + + void setIcon(QIcon icon); + +private: + QSize m_size; + QIcon m_icon; +}; diff --git a/ultimmc/launcher/ui/widgets/InstanceCardWidget.ui b/ultimmc/launcher/ui/widgets/InstanceCardWidget.ui new file mode 100644 index 0000000..6eeeb07 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/InstanceCardWidget.ui @@ -0,0 +1,58 @@ + + + InstanceCardWidget + + + + 0 + 0 + 473 + 118 + + + + + + + + 80 + 80 + + + + + + + + &Name: + + + instNameTextBox + + + + + + + + + + &Group: + + + groupBox + + + + + + + true + + + + + + + + diff --git a/ultimmc/launcher/ui/widgets/JavaSettingsWidget.cpp b/ultimmc/launcher/ui/widgets/JavaSettingsWidget.cpp new file mode 100644 index 0000000..ed07e08 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/JavaSettingsWidget.cpp @@ -0,0 +1,432 @@ +#include "JavaSettingsWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "java/JavaInstall.h" +#include "java/JavaUtils.h" +#include "FileSystem.h" + +#include "ui/dialogs/CustomMessageBox.h" +#include "ui/widgets/VersionSelectWidget.h" + +#include "Application.h" +#include "BuildConfig.h" + +JavaSettingsWidget::JavaSettingsWidget(QWidget* parent) : QWidget(parent) +{ + m_availableMemory = Sys::getSystemRam() / Sys::mebibyte; + + goodIcon = APPLICATION->getThemedIcon("status-good"); + yellowIcon = APPLICATION->getThemedIcon("status-yellow"); + badIcon = APPLICATION->getThemedIcon("status-bad"); + setupUi(); + + connect(m_minMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryValueChanged(int))); + connect(m_maxMemSpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryValueChanged(int))); + connect(m_permGenSpinBox, SIGNAL(valueChanged(int)), this, SLOT(memoryValueChanged(int))); + connect(m_versionWidget, &VersionSelectWidget::selectedVersionChanged, this, &JavaSettingsWidget::javaVersionSelected); + connect(m_javaBrowseBtn, &QPushButton::clicked, this, &JavaSettingsWidget::on_javaBrowseBtn_clicked); + connect(m_javaPathTextBox, &QLineEdit::textEdited, this, &JavaSettingsWidget::javaPathEdited); + connect(m_javaStatusBtn, &QToolButton::clicked, this, &JavaSettingsWidget::on_javaStatusBtn_clicked); +} + +void JavaSettingsWidget::setupUi() +{ + setObjectName(QStringLiteral("javaSettingsWidget")); + m_verticalLayout = new QVBoxLayout(this); + m_verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + + m_versionWidget = new VersionSelectWidget(this); + m_verticalLayout->addWidget(m_versionWidget); + + m_horizontalLayout = new QHBoxLayout(); + m_horizontalLayout->setObjectName(QStringLiteral("horizontalLayout")); + m_javaPathTextBox = new QLineEdit(this); + m_javaPathTextBox->setObjectName(QStringLiteral("javaPathTextBox")); + + m_horizontalLayout->addWidget(m_javaPathTextBox); + + m_javaBrowseBtn = new QPushButton(this); + m_javaBrowseBtn->setObjectName(QStringLiteral("javaBrowseBtn")); + + m_horizontalLayout->addWidget(m_javaBrowseBtn); + + m_javaStatusBtn = new QToolButton(this); + m_javaStatusBtn->setIcon(yellowIcon); + m_horizontalLayout->addWidget(m_javaStatusBtn); + + m_verticalLayout->addLayout(m_horizontalLayout); + + m_memoryGroupBox = new QGroupBox(this); + m_memoryGroupBox->setObjectName(QStringLiteral("memoryGroupBox")); + m_gridLayout_2 = new QGridLayout(m_memoryGroupBox); + m_gridLayout_2->setObjectName(QStringLiteral("gridLayout_2")); + + m_labelMinMem = new QLabel(m_memoryGroupBox); + m_labelMinMem->setObjectName(QStringLiteral("labelMinMem")); + m_gridLayout_2->addWidget(m_labelMinMem, 0, 0, 1, 1); + + m_minMemSpinBox = new QSpinBox(m_memoryGroupBox); + m_minMemSpinBox->setObjectName(QStringLiteral("minMemSpinBox")); + m_minMemSpinBox->setSuffix(QStringLiteral(" MiB")); + m_minMemSpinBox->setMinimum(128); + m_minMemSpinBox->setMaximum(m_availableMemory); + m_minMemSpinBox->setSingleStep(128); + m_labelMinMem->setBuddy(m_minMemSpinBox); + m_gridLayout_2->addWidget(m_minMemSpinBox, 0, 1, 1, 1); + + m_labelMaxMem = new QLabel(m_memoryGroupBox); + m_labelMaxMem->setObjectName(QStringLiteral("labelMaxMem")); + m_gridLayout_2->addWidget(m_labelMaxMem, 1, 0, 1, 1); + + m_maxMemSpinBox = new QSpinBox(m_memoryGroupBox); + m_maxMemSpinBox->setObjectName(QStringLiteral("maxMemSpinBox")); + m_maxMemSpinBox->setSuffix(QStringLiteral(" MiB")); + m_maxMemSpinBox->setMinimum(128); + m_maxMemSpinBox->setMaximum(m_availableMemory); + m_maxMemSpinBox->setSingleStep(128); + m_labelMaxMem->setBuddy(m_maxMemSpinBox); + m_gridLayout_2->addWidget(m_maxMemSpinBox, 1, 1, 1, 1); + + m_labelPermGen = new QLabel(m_memoryGroupBox); + m_labelPermGen->setObjectName(QStringLiteral("labelPermGen")); + m_labelPermGen->setText(QStringLiteral("PermGen:")); + m_gridLayout_2->addWidget(m_labelPermGen, 2, 0, 1, 1); + m_labelPermGen->setVisible(false); + + m_permGenSpinBox = new QSpinBox(m_memoryGroupBox); + m_permGenSpinBox->setObjectName(QStringLiteral("permGenSpinBox")); + m_permGenSpinBox->setSuffix(QStringLiteral(" MiB")); + m_permGenSpinBox->setMinimum(64); + m_permGenSpinBox->setMaximum(m_availableMemory); + m_permGenSpinBox->setSingleStep(8); + m_gridLayout_2->addWidget(m_permGenSpinBox, 2, 1, 1, 1); + m_permGenSpinBox->setVisible(false); + + m_verticalLayout->addWidget(m_memoryGroupBox); + + retranslate(); +} + +void JavaSettingsWidget::initialize() +{ + m_versionWidget->initialize(APPLICATION->javalist().get()); + m_versionWidget->setResizeOn(2); + auto s = APPLICATION->settings(); + // Memory + observedMinMemory = s->get("MinMemAlloc").toInt(); + observedMaxMemory = s->get("MaxMemAlloc").toInt(); + observedPermGenMemory = s->get("PermGen").toInt(); + m_minMemSpinBox->setValue(observedMinMemory); + m_maxMemSpinBox->setValue(observedMaxMemory); + m_permGenSpinBox->setValue(observedPermGenMemory); +} + +void JavaSettingsWidget::refresh() +{ + m_versionWidget->loadList(); +} + +JavaSettingsWidget::ValidationStatus JavaSettingsWidget::validate() +{ + switch(javaStatus) + { + default: + case JavaStatus::NotSet: + case JavaStatus::DoesNotExist: + case JavaStatus::DoesNotStart: + case JavaStatus::ReturnedInvalidData: + { + int button = CustomMessageBox::selectable( + this, + tr("No Java version selected"), + tr("You didn't select a Java version or selected something that doesn't work.\n" + "%1 will not be able to start Minecraft.\n" + "Do you wish to proceed without any Java?" + "\n\n" + "You can change the Java version in the settings later.\n" + ).arg(BuildConfig.LAUNCHER_NAME), + QMessageBox::Warning, + QMessageBox::Yes | QMessageBox::No, + QMessageBox::NoButton + )->exec(); + if(button == QMessageBox::No) + { + return ValidationStatus::Bad; + } + return ValidationStatus::JavaBad; + } + break; + case JavaStatus::Pending: + { + return ValidationStatus::Bad; + } + case JavaStatus::Good: + { + return ValidationStatus::AllOK; + } + } +} + +QString JavaSettingsWidget::javaPath() const +{ + return m_javaPathTextBox->text(); +} + +int JavaSettingsWidget::maxHeapSize() const +{ + return m_maxMemSpinBox->value(); +} + +int JavaSettingsWidget::minHeapSize() const +{ + return m_minMemSpinBox->value(); +} + +bool JavaSettingsWidget::permGenEnabled() const +{ + return m_permGenSpinBox->isVisible(); +} + +int JavaSettingsWidget::permGenSize() const +{ + return m_permGenSpinBox->value(); +} + +void JavaSettingsWidget::memoryValueChanged(int) +{ + bool actuallyChanged = false; + int min = m_minMemSpinBox->value(); + int max = m_maxMemSpinBox->value(); + int permgen = m_permGenSpinBox->value(); + QObject *obj = sender(); + if (obj == m_minMemSpinBox && min != observedMinMemory) + { + observedMinMemory = min; + actuallyChanged = true; + if (min > max) + { + observedMaxMemory = min; + m_maxMemSpinBox->setValue(min); + } + } + else if (obj == m_maxMemSpinBox && max != observedMaxMemory) + { + observedMaxMemory = max; + actuallyChanged = true; + if (min > max) + { + observedMinMemory = max; + m_minMemSpinBox->setValue(max); + } + } + else if (obj == m_permGenSpinBox && permgen != observedPermGenMemory) + { + observedPermGenMemory = permgen; + actuallyChanged = true; + } + if(actuallyChanged) + { + checkJavaPathOnEdit(m_javaPathTextBox->text()); + } +} + +void JavaSettingsWidget::javaVersionSelected(BaseVersionPtr version) +{ + auto java = std::dynamic_pointer_cast(version); + if(!java) + { + return; + } + auto visible = java->id.requiresPermGen(); + m_labelPermGen->setVisible(visible); + m_permGenSpinBox->setVisible(visible); + m_javaPathTextBox->setText(java->path); + checkJavaPath(java->path); +} + +void JavaSettingsWidget::on_javaBrowseBtn_clicked() +{ + QString filter; +#if defined Q_OS_WIN32 + filter = "Java (javaw.exe)"; +#else + filter = "Java (java)"; +#endif + QString raw_path = QFileDialog::getOpenFileName(this, tr("Find Java executable"), QString(), filter); + if(raw_path.isEmpty()) + { + return; + } + QString cooked_path = FS::NormalizePath(raw_path); + m_javaPathTextBox->setText(cooked_path); + checkJavaPath(cooked_path); +} + +void JavaSettingsWidget::on_javaStatusBtn_clicked() +{ + QString text; + bool failed = false; + switch(javaStatus) + { + case JavaStatus::NotSet: + checkJavaPath(m_javaPathTextBox->text()); + return; + case JavaStatus::DoesNotExist: + text += QObject::tr("The specified file either doesn't exist or is not a proper executable."); + failed = true; + break; + case JavaStatus::DoesNotStart: + { + text += QObject::tr("The specified java binary didn't start properly.
"); + auto htmlError = m_result.errorLog; + if(!htmlError.isEmpty()) + { + htmlError.replace('\n', "
"); + text += QString("%1").arg(htmlError); + } + failed = true; + break; + } + case JavaStatus::ReturnedInvalidData: + { + text += QObject::tr("The specified java binary returned unexpected results:
"); + auto htmlOut = m_result.outLog; + if(!htmlOut.isEmpty()) + { + htmlOut.replace('\n', "
"); + text += QString("%1").arg(htmlOut); + } + failed = true; + break; + } + case JavaStatus::Good: + text += QObject::tr("Java test succeeded!
Platform reported: %1
Java version " + "reported: %2
").arg(m_result.realPlatform, m_result.javaVersion.toString()); + break; + case JavaStatus::Pending: + // TODO: abort here? + return; + } + CustomMessageBox::selectable( + this, + failed ? QObject::tr("Java test failure") : QObject::tr("Java test success"), + text, + failed ? QMessageBox::Critical : QMessageBox::Information + )->show(); +} + +void JavaSettingsWidget::setJavaStatus(JavaSettingsWidget::JavaStatus status) +{ + javaStatus = status; + switch(javaStatus) + { + case JavaStatus::Good: + m_javaStatusBtn->setIcon(goodIcon); + break; + case JavaStatus::NotSet: + case JavaStatus::Pending: + m_javaStatusBtn->setIcon(yellowIcon); + break; + default: + m_javaStatusBtn->setIcon(badIcon); + break; + } +} + +void JavaSettingsWidget::javaPathEdited(const QString& path) +{ + checkJavaPathOnEdit(path); +} + +void JavaSettingsWidget::checkJavaPathOnEdit(const QString& path) +{ + auto realPath = FS::ResolveExecutable(path); + QFileInfo pathInfo(realPath); + if (pathInfo.baseName().toLower().contains("java")) + { + checkJavaPath(path); + } + else + { + if(!m_checker) + { + setJavaStatus(JavaStatus::NotSet); + } + } +} + +void JavaSettingsWidget::checkJavaPath(const QString &path) +{ + if(m_checker) + { + queuedCheck = path; + return; + } + auto realPath = FS::ResolveExecutable(path); + if(realPath.isNull()) + { + setJavaStatus(JavaStatus::DoesNotExist); + return; + } + setJavaStatus(JavaStatus::Pending); + m_checker.reset(new JavaChecker()); + m_checker->m_path = path; + m_checker->m_minMem = m_minMemSpinBox->value(); + m_checker->m_maxMem = m_maxMemSpinBox->value(); + if(m_permGenSpinBox->isVisible()) + { + m_checker->m_permGen = m_permGenSpinBox->value(); + } + connect(m_checker.get(), &JavaChecker::checkFinished, this, &JavaSettingsWidget::checkFinished); + m_checker->performCheck(); +} + +void JavaSettingsWidget::checkFinished(JavaCheckResult result) +{ + m_result = result; + switch(result.validity) + { + case JavaCheckResult::Validity::Valid: + { + setJavaStatus(JavaStatus::Good); + break; + } + case JavaCheckResult::Validity::ReturnedInvalidData: + { + setJavaStatus(JavaStatus::ReturnedInvalidData); + break; + } + case JavaCheckResult::Validity::Errored: + { + setJavaStatus(JavaStatus::DoesNotStart); + break; + } + } + m_checker.reset(); + if(!queuedCheck.isNull()) + { + checkJavaPath(queuedCheck); + queuedCheck.clear(); + } +} + +void JavaSettingsWidget::retranslate() +{ + m_memoryGroupBox->setTitle(tr("Memory")); + m_maxMemSpinBox->setToolTip(tr("The maximum amount of memory Minecraft is allowed to use.")); + m_labelMinMem->setText(tr("Minimum memory allocation:")); + m_labelMaxMem->setText(tr("Maximum memory allocation:")); + m_minMemSpinBox->setToolTip(tr("The amount of memory Minecraft is started with.")); + m_permGenSpinBox->setToolTip(tr("The amount of memory available to store loaded Java classes.")); + m_javaBrowseBtn->setText(tr("Browse")); +} diff --git a/ultimmc/launcher/ui/widgets/JavaSettingsWidget.h b/ultimmc/launcher/ui/widgets/JavaSettingsWidget.h new file mode 100644 index 0000000..0d280da --- /dev/null +++ b/ultimmc/launcher/ui/widgets/JavaSettingsWidget.h @@ -0,0 +1,102 @@ +#pragma once +#include + +#include +#include +#include +#include + +class QLineEdit; +class VersionSelectWidget; +class QSpinBox; +class QPushButton; +class QVBoxLayout; +class QHBoxLayout; +class QGroupBox; +class QGridLayout; +class QLabel; +class QToolButton; + +/** + * This is a widget for all the Java settings dialogs and pages. + */ +class JavaSettingsWidget : public QWidget +{ + Q_OBJECT + +public: + explicit JavaSettingsWidget(QWidget *parent); + virtual ~JavaSettingsWidget() {}; + + enum class JavaStatus + { + NotSet, + Pending, + Good, + DoesNotExist, + DoesNotStart, + ReturnedInvalidData + } javaStatus = JavaStatus::NotSet; + + enum class ValidationStatus + { + Bad, + JavaBad, + AllOK + }; + + void refresh(); + void initialize(); + ValidationStatus validate(); + void retranslate(); + + bool permGenEnabled() const; + int permGenSize() const; + int minHeapSize() const; + int maxHeapSize() const; + QString javaPath() const; + + +protected slots: + void memoryValueChanged(int); + void javaPathEdited(const QString &path); + void javaVersionSelected(BaseVersionPtr version); + void on_javaBrowseBtn_clicked(); + void on_javaStatusBtn_clicked(); + void checkFinished(JavaCheckResult result); + +protected: /* methods */ + void checkJavaPathOnEdit(const QString &path); + void checkJavaPath(const QString &path); + void setJavaStatus(JavaStatus status); + void setupUi(); + +private: /* data */ + VersionSelectWidget *m_versionWidget = nullptr; + QVBoxLayout *m_verticalLayout = nullptr; + + QLineEdit * m_javaPathTextBox = nullptr; + QPushButton * m_javaBrowseBtn = nullptr; + QToolButton * m_javaStatusBtn = nullptr; + QHBoxLayout *m_horizontalLayout = nullptr; + + QGroupBox *m_memoryGroupBox = nullptr; + QGridLayout *m_gridLayout_2 = nullptr; + QSpinBox *m_maxMemSpinBox = nullptr; + QLabel *m_labelMinMem = nullptr; + QLabel *m_labelMaxMem = nullptr; + QSpinBox *m_minMemSpinBox = nullptr; + QLabel *m_labelPermGen = nullptr; + QSpinBox *m_permGenSpinBox = nullptr; + QIcon goodIcon; + QIcon yellowIcon; + QIcon badIcon; + + int observedMinMemory = 0; + int observedMaxMemory = 0; + int observedPermGenMemory = 0; + QString queuedCheck; + uint64_t m_availableMemory = 0ull; + shared_qobject_ptr m_checker; + JavaCheckResult m_result; +}; diff --git a/ultimmc/launcher/ui/widgets/LabeledToolButton.cpp b/ultimmc/launcher/ui/widgets/LabeledToolButton.cpp new file mode 100644 index 0000000..ab2d327 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/LabeledToolButton.cpp @@ -0,0 +1,115 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include "LabeledToolButton.h" +#include +#include + +/* + * + * Tool Button with a label on it, instead of the normal text rendering + * + */ + +LabeledToolButton::LabeledToolButton(QWidget * parent) + : QToolButton(parent) + , m_label(new QLabel(this)) +{ + //QToolButton::setText(" "); + m_label->setWordWrap(true); + m_label->setMouseTracking(false); + m_label->setAlignment(Qt::AlignCenter); + m_label->setTextInteractionFlags(Qt::NoTextInteraction); + // somehow, this makes word wrap work in the QLabel. yay. + //m_label->setMinimumWidth(100); +} + +QString LabeledToolButton::text() const +{ + return m_label->text(); +} + +void LabeledToolButton::setText(const QString & text) +{ + m_label->setText(text); +} + +void LabeledToolButton::setIcon(QIcon icon) +{ + m_icon = icon; + resetIcon(); +} + + +/*! + \reimp +*/ +QSize LabeledToolButton::sizeHint() const +{ + /* + Q_D(const QToolButton); + if (d->sizeHint.isValid()) + return d->sizeHint; + */ + ensurePolished(); + + int w = 0, h = 0; + QStyleOptionToolButton opt; + initStyleOption(&opt); + QSize sz =m_label->sizeHint(); + w = sz.width(); + h = sz.height(); + + opt.rect.setSize(QSize(w, h)); // PM_MenuButtonIndicator depends on the height + if (popupMode() == MenuButtonPopup) + w += style()->pixelMetric(QStyle::PM_MenuButtonIndicator, &opt, this); + + QSize rawSize = style()->sizeFromContents(QStyle::CT_ToolButton, &opt, QSize(w, h), this); + QSize sizeHint = rawSize.expandedTo(QApplication::globalStrut()); + return sizeHint; +} + + + +void LabeledToolButton::resizeEvent(QResizeEvent * event) +{ + m_label->setGeometry(QRect(4, 4, width()-8, height()-8)); + if(!m_icon.isNull()) + { + resetIcon(); + } + QWidget::resizeEvent(event); +} + +void LabeledToolButton::resetIcon() +{ + auto iconSz = m_icon.actualSize(QSize(160, 80)); + float w = iconSz.width(); + float h = iconSz.height(); + float ar = w/h; + // FIXME: hardcoded max size of 160x80 + int newW = 80 * ar; + if(newW > 160) + newW = 160; + QSize newSz (newW, 80); + auto pixmap = m_icon.pixmap(newSz); + m_label->setPixmap(pixmap); + m_label->setMinimumHeight(80); + m_label->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred ); +} diff --git a/ultimmc/launcher/ui/widgets/LabeledToolButton.h b/ultimmc/launcher/ui/widgets/LabeledToolButton.h new file mode 100644 index 0000000..51f99e9 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/LabeledToolButton.h @@ -0,0 +1,40 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +class QLabel; + +class LabeledToolButton : public QToolButton +{ + Q_OBJECT + + QLabel * m_label; + QIcon m_icon; + +public: + LabeledToolButton(QWidget * parent = 0); + + QString text() const; + void setText(const QString & text); + void setIcon(QIcon icon); + virtual QSize sizeHint() const; +protected: + void resizeEvent(QResizeEvent * event); + void resetIcon(); +}; diff --git a/ultimmc/launcher/ui/widgets/LanguageSelectionWidget.cpp b/ultimmc/launcher/ui/widgets/LanguageSelectionWidget.cpp new file mode 100644 index 0000000..cf70c7b --- /dev/null +++ b/ultimmc/launcher/ui/widgets/LanguageSelectionWidget.cpp @@ -0,0 +1,66 @@ +#include "LanguageSelectionWidget.h" + +#include +#include +#include +#include +#include "Application.h" +#include "translations/TranslationsModel.h" + +LanguageSelectionWidget::LanguageSelectionWidget(QWidget *parent) : + QWidget(parent) +{ + verticalLayout = new QVBoxLayout(this); + verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + languageView = new QTreeView(this); + languageView->setObjectName(QStringLiteral("languageView")); + languageView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + languageView->setAlternatingRowColors(true); + languageView->setRootIsDecorated(false); + languageView->setItemsExpandable(false); + languageView->setWordWrap(true); + languageView->header()->setCascadingSectionResizes(true); + languageView->header()->setStretchLastSection(false); + verticalLayout->addWidget(languageView); + helpUsLabel = new QLabel(this); + helpUsLabel->setObjectName(QStringLiteral("helpUsLabel")); + helpUsLabel->setTextInteractionFlags(Qt::LinksAccessibleByMouse); + helpUsLabel->setOpenExternalLinks(true); + helpUsLabel->setWordWrap(true); + verticalLayout->addWidget(helpUsLabel); + + auto translations = APPLICATION->translations(); + auto index = translations->selectedIndex(); + languageView->setModel(translations.get()); + languageView->setCurrentIndex(index); + languageView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + languageView->header()->setSectionResizeMode(0, QHeaderView::Stretch); + connect(languageView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &LanguageSelectionWidget::languageRowChanged); + verticalLayout->setContentsMargins(0,0,0,0); +} + +QString LanguageSelectionWidget::getSelectedLanguageKey() const +{ + auto translations = APPLICATION->translations(); + return translations->data(languageView->currentIndex(), Qt::UserRole).toString(); +} + +void LanguageSelectionWidget::retranslate() +{ + QString text = tr("Don't see your language or the quality is poor?
Help us with translations!") + .arg("https://github.com/MultiMC/Launcher/wiki/Translating-MultiMC"); + helpUsLabel->setText(text); + +} + +void LanguageSelectionWidget::languageRowChanged(const QModelIndex& current, const QModelIndex& previous) +{ + if (current == previous) + { + return; + } + auto translations = APPLICATION->translations(); + QString key = translations->data(current, Qt::UserRole).toString(); + translations->selectLanguage(key); + translations->updateLanguage(key); +} diff --git a/ultimmc/launcher/ui/widgets/LanguageSelectionWidget.h b/ultimmc/launcher/ui/widgets/LanguageSelectionWidget.h new file mode 100644 index 0000000..e65936d --- /dev/null +++ b/ultimmc/launcher/ui/widgets/LanguageSelectionWidget.h @@ -0,0 +1,41 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +class QVBoxLayout; +class QTreeView; +class QLabel; + +class LanguageSelectionWidget: public QWidget +{ + Q_OBJECT +public: + explicit LanguageSelectionWidget(QWidget *parent = 0); + virtual ~LanguageSelectionWidget() { }; + + QString getSelectedLanguageKey() const; + void retranslate(); + +protected slots: + void languageRowChanged(const QModelIndex ¤t, const QModelIndex &previous); + +private: + QVBoxLayout *verticalLayout = nullptr; + QTreeView *languageView = nullptr; + QLabel *helpUsLabel = nullptr; +}; diff --git a/ultimmc/launcher/ui/widgets/LineSeparator.cpp b/ultimmc/launcher/ui/widgets/LineSeparator.cpp new file mode 100644 index 0000000..d03e676 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/LineSeparator.cpp @@ -0,0 +1,37 @@ +#include "LineSeparator.h" + +#include +#include +#include +#include + +void LineSeparator::initStyleOption(QStyleOption *option) const +{ + option->initFrom(this); + // in a horizontal layout, the line is vertical (and vice versa) + if (m_orientation == Qt::Vertical) + option->state |= QStyle::State_Horizontal; +} + +LineSeparator::LineSeparator(QWidget *parent, Qt::Orientation orientation) + : QWidget(parent), m_orientation(orientation) +{ + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); +} + +QSize LineSeparator::sizeHint() const +{ + QStyleOption opt; + initStyleOption(&opt); + const int extent = + style()->pixelMetric(QStyle::PM_ToolBarSeparatorExtent, &opt, parentWidget()); + return QSize(extent, extent); +} + +void LineSeparator::paintEvent(QPaintEvent *) +{ + QPainter p(this); + QStyleOption opt; + initStyleOption(&opt); + style()->drawPrimitive(QStyle::PE_IndicatorToolBarSeparator, &opt, &p, parentWidget()); +} diff --git a/ultimmc/launcher/ui/widgets/LineSeparator.h b/ultimmc/launcher/ui/widgets/LineSeparator.h new file mode 100644 index 0000000..22927b6 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/LineSeparator.h @@ -0,0 +1,18 @@ +#pragma once +#include + +class QStyleOption; + +class LineSeparator : public QWidget +{ + Q_OBJECT + +public: + /// Create a line separator. orientation is the orientation of the line. + explicit LineSeparator(QWidget *parent, Qt::Orientation orientation = Qt::Horizontal); + QSize sizeHint() const; + void paintEvent(QPaintEvent *); + void initStyleOption(QStyleOption *option) const; +private: + Qt::Orientation m_orientation = Qt::Horizontal; +}; diff --git a/ultimmc/launcher/ui/widgets/LogView.cpp b/ultimmc/launcher/ui/widgets/LogView.cpp new file mode 100644 index 0000000..26a2a52 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/LogView.cpp @@ -0,0 +1,144 @@ +#include "LogView.h" +#include +#include + +LogView::LogView(QWidget* parent) : QPlainTextEdit(parent) +{ + setWordWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); + m_defaultFormat = new QTextCharFormat(currentCharFormat()); +} + +LogView::~LogView() +{ + delete m_defaultFormat; +} + +void LogView::setWordWrap(bool wrapping) +{ + if(wrapping) + { + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + setLineWrapMode(QPlainTextEdit::WidgetWidth); + } + else + { + setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); + setLineWrapMode(QPlainTextEdit::NoWrap); + } +} + +void LogView::setModel(QAbstractItemModel* model) +{ + if(m_model) + { + disconnect(m_model, &QAbstractItemModel::modelReset, this, &LogView::repopulate); + disconnect(m_model, &QAbstractItemModel::rowsInserted, this, &LogView::rowsInserted); + disconnect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this, &LogView::rowsAboutToBeInserted); + disconnect(m_model, &QAbstractItemModel::rowsRemoved, this, &LogView::rowsRemoved); + } + m_model = model; + if(m_model) + { + connect(m_model, &QAbstractItemModel::modelReset, this, &LogView::repopulate); + connect(m_model, &QAbstractItemModel::rowsInserted, this, &LogView::rowsInserted); + connect(m_model, &QAbstractItemModel::rowsAboutToBeInserted, this, &LogView::rowsAboutToBeInserted); + connect(m_model, &QAbstractItemModel::rowsRemoved, this, &LogView::rowsRemoved); + connect(m_model, &QAbstractItemModel::destroyed, this, &LogView::modelDestroyed); + } + repopulate(); +} + +QAbstractItemModel * LogView::model() const +{ + return m_model; +} + +void LogView::modelDestroyed(QObject* model) +{ + if(m_model == model) + { + setModel(nullptr); + } +} + +void LogView::repopulate() +{ + auto doc = document(); + doc->clear(); + if(!m_model) + { + return; + } + rowsInserted(QModelIndex(), 0, m_model->rowCount() - 1); +} + +void LogView::rowsAboutToBeInserted(const QModelIndex& parent, int first, int last) +{ + Q_UNUSED(parent) + Q_UNUSED(first) + Q_UNUSED(last) + QScrollBar *bar = verticalScrollBar(); + int max_bar = bar->maximum(); + int val_bar = bar->value(); + if (m_scroll) + { + m_scroll = (max_bar - val_bar) <= 1; + } + else + { + m_scroll = val_bar == max_bar; + } +} + +void LogView::rowsInserted(const QModelIndex& parent, int first, int last) +{ + for(int i = first; i <= last; i++) + { + auto idx = m_model->index(i, 0, parent); + auto text = m_model->data(idx, Qt::DisplayRole).toString(); + QTextCharFormat format(*m_defaultFormat); + auto font = m_model->data(idx, Qt::FontRole); + if(font.isValid()) + { + format.setFont(font.value()); + } + auto fg = m_model->data(idx, Qt::TextColorRole); + if(fg.isValid()) + { + format.setForeground(fg.value()); + } + auto bg = m_model->data(idx, Qt::BackgroundRole); + if(bg.isValid()) + { + format.setBackground(bg.value()); + } + auto workCursor = textCursor(); + workCursor.movePosition(QTextCursor::End); + workCursor.insertText(text, format); + workCursor.insertBlock(); + } + if(m_scroll && !m_scrolling) + { + m_scrolling = true; + QMetaObject::invokeMethod( this, "scrollToBottom", Qt::QueuedConnection); + } +} + +void LogView::rowsRemoved(const QModelIndex& parent, int first, int last) +{ + // TODO: some day... maybe + Q_UNUSED(parent) + Q_UNUSED(first) + Q_UNUSED(last) +} + +void LogView::scrollToBottom() +{ + m_scrolling = false; + verticalScrollBar()->setSliderPosition(verticalScrollBar()->maximum()); +} + +void LogView::findNext(const QString& what, bool reverse) +{ + find(what, reverse ? QTextDocument::FindFlag::FindBackward : QTextDocument::FindFlag(0)); +} diff --git a/ultimmc/launcher/ui/widgets/LogView.h b/ultimmc/launcher/ui/widgets/LogView.h new file mode 100644 index 0000000..3143360 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/LogView.h @@ -0,0 +1,36 @@ +#pragma once +#include +#include + +class QAbstractItemModel; + +class LogView: public QPlainTextEdit +{ + Q_OBJECT +public: + explicit LogView(QWidget *parent = nullptr); + virtual ~LogView(); + + virtual void setModel(QAbstractItemModel *model); + QAbstractItemModel *model() const; + +public slots: + void setWordWrap(bool wrapping); + void findNext(const QString & what, bool reverse); + void scrollToBottom(); + +protected slots: + void repopulate(); + // note: this supports only appending + void rowsInserted(const QModelIndex &parent, int first, int last); + void rowsAboutToBeInserted(const QModelIndex &parent, int first, int last); + // note: this supports only removing from front + void rowsRemoved(const QModelIndex &parent, int first, int last); + void modelDestroyed(QObject * model); + +protected: + QAbstractItemModel *m_model = nullptr; + QTextCharFormat *m_defaultFormat = nullptr; + bool m_scroll = false; + bool m_scrolling = false; +}; diff --git a/ultimmc/launcher/ui/widgets/MCModInfoFrame.cpp b/ultimmc/launcher/ui/widgets/MCModInfoFrame.cpp new file mode 100644 index 0000000..8c4bd69 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/MCModInfoFrame.cpp @@ -0,0 +1,168 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "MCModInfoFrame.h" +#include "ui_MCModInfoFrame.h" + +#include "ui/dialogs/CustomMessageBox.h" + +void MCModInfoFrame::updateWithMod(Mod &m) +{ + if (m.type() == m.MOD_FOLDER) + { + clear(); + return; + } + + QString text = ""; + QString name = ""; + if (m.name().isEmpty()) + name = m.mmc_id(); + else + name = m.name(); + + if (m.homeurl().isEmpty()) + text = name; + else + text = "" + name + ""; + if (!m.authors().isEmpty()) + text += " by " + m.authors().join(", "); + + setModText(text); + + if (m.description().isEmpty()) + { + setModDescription(QString()); + } + else + { + setModDescription(m.description()); + } +} + +void MCModInfoFrame::clear() +{ + setModText(QString()); + setModDescription(QString()); +} + +MCModInfoFrame::MCModInfoFrame(QWidget *parent) : + QFrame(parent), + ui(new Ui::MCModInfoFrame) +{ + ui->setupUi(this); + ui->label_ModDescription->setHidden(true); + ui->label_ModText->setHidden(true); + updateHiddenState(); +} + +MCModInfoFrame::~MCModInfoFrame() +{ + delete ui; +} + +void MCModInfoFrame::updateHiddenState() +{ + if(ui->label_ModDescription->isHidden() && ui->label_ModText->isHidden()) + { + setHidden(true); + } + else + { + setHidden(false); + } +} + +void MCModInfoFrame::setModText(QString text) +{ + if(text.isEmpty()) + { + ui->label_ModText->setHidden(true); + } + else + { + ui->label_ModText->setText(text); + ui->label_ModText->setHidden(false); + } + updateHiddenState(); +} + +void MCModInfoFrame::setModDescription(QString text) +{ + if(text.isEmpty()) + { + ui->label_ModDescription->setHidden(true); + updateHiddenState(); + return; + } + else + { + ui->label_ModDescription->setHidden(false); + updateHiddenState(); + } + ui->label_ModDescription->setToolTip(""); + QString intermediatetext = text.trimmed(); + bool prev(false); + QChar rem('\n'); + QString finaltext; + finaltext.reserve(intermediatetext.size()); + foreach(const QChar& c, intermediatetext) + { + if(c == rem && prev){ + continue; + } + prev = c == rem; + finaltext += c; + } + QString labeltext; + labeltext.reserve(300); + if(finaltext.length() > 290) + { + ui->label_ModDescription->setOpenExternalLinks(false); + ui->label_ModDescription->setTextFormat(Qt::TextFormat::RichText); + desc = text; + // This allows injecting HTML here. + labeltext.append("" + finaltext.left(287) + "..."); + QObject::connect(ui->label_ModDescription, &QLabel::linkActivated, this, &MCModInfoFrame::modDescEllipsisHandler); + } + else + { + ui->label_ModDescription->setTextFormat(Qt::TextFormat::PlainText); + labeltext.append(finaltext); + } + ui->label_ModDescription->setText(labeltext); +} + +void MCModInfoFrame::modDescEllipsisHandler(const QString &link) +{ + if(!currentBox) + { + currentBox = CustomMessageBox::selectable(this, QString(), desc); + connect(currentBox, &QMessageBox::finished, this, &MCModInfoFrame::boxClosed); + currentBox->show(); + } + else + { + currentBox->setText(desc); + } +} + +void MCModInfoFrame::boxClosed(int result) +{ + currentBox = nullptr; +} diff --git a/ultimmc/launcher/ui/widgets/MCModInfoFrame.h b/ultimmc/launcher/ui/widgets/MCModInfoFrame.h new file mode 100644 index 0000000..0b7ef53 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/MCModInfoFrame.h @@ -0,0 +1,52 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include "minecraft/mod/Mod.h" + +namespace Ui +{ +class MCModInfoFrame; +} + +class MCModInfoFrame : public QFrame +{ + Q_OBJECT + +public: + explicit MCModInfoFrame(QWidget *parent = 0); + ~MCModInfoFrame(); + + void setModText(QString text); + void setModDescription(QString text); + + void updateWithMod(Mod &m); + void clear(); + +public slots: + void modDescEllipsisHandler(const QString& link ); + void boxClosed(int result); + +private: + void updateHiddenState(); + +private: + Ui::MCModInfoFrame *ui; + QString desc; + class QMessageBox * currentBox = nullptr; +}; + diff --git a/ultimmc/launcher/ui/widgets/MCModInfoFrame.ui b/ultimmc/launcher/ui/widgets/MCModInfoFrame.ui new file mode 100644 index 0000000..5ef3337 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/MCModInfoFrame.ui @@ -0,0 +1,92 @@ + + + MCModInfoFrame + + + + 0 + 0 + 527 + 113 + + + + + 0 + 0 + + + + + 16777215 + 120 + + + + + 6 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + + + + + + Qt::RichText + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + true + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + + diff --git a/ultimmc/launcher/ui/widgets/ModListView.cpp b/ultimmc/launcher/ui/widgets/ModListView.cpp new file mode 100644 index 0000000..c8ccd29 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/ModListView.cpp @@ -0,0 +1,66 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "ModListView.h" +#include +#include +#include +#include +#include + +ModListView::ModListView ( QWidget* parent ) + :QTreeView ( parent ) +{ + setAllColumnsShowFocus ( true ); + setExpandsOnDoubleClick ( false ); + setRootIsDecorated ( false ); + setSortingEnabled ( true ); + setAlternatingRowColors ( true ); + setSelectionMode ( QAbstractItemView::ExtendedSelection ); + setHeaderHidden ( false ); + setSelectionBehavior(QAbstractItemView::SelectRows); + setVerticalScrollBarPolicy ( Qt::ScrollBarAlwaysOn ); + setHorizontalScrollBarPolicy ( Qt::ScrollBarAsNeeded ); + setDropIndicatorShown(true); + setDragEnabled(true); + setDragDropMode(QAbstractItemView::DropOnly); + viewport()->setAcceptDrops(true); +} + +void ModListView::setModel ( QAbstractItemModel* model ) +{ + QTreeView::setModel ( model ); + auto head = header(); + head->setStretchLastSection(false); + // HACK: this is true for the checkbox column of mod lists + auto string = model->headerData(0,head->orientation()).toString(); + if(head->count() < 1) + { + return; + } + if(!string.size()) + { + head->setSectionResizeMode(0, QHeaderView::ResizeToContents); + head->setSectionResizeMode(1, QHeaderView::Stretch); + for(int i = 2; i < head->count(); i++) + head->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + else + { + head->setSectionResizeMode(0, QHeaderView::Stretch); + for(int i = 1; i < head->count(); i++) + head->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } +} diff --git a/ultimmc/launcher/ui/widgets/ModListView.h b/ultimmc/launcher/ui/widgets/ModListView.h new file mode 100644 index 0000000..881e092 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/ModListView.h @@ -0,0 +1,25 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +class ModListView: public QTreeView +{ + Q_OBJECT +public: + explicit ModListView ( QWidget* parent = 0 ); + virtual void setModel ( QAbstractItemModel* model ); +}; diff --git a/ultimmc/launcher/ui/widgets/PageContainer.cpp b/ultimmc/launcher/ui/widgets/PageContainer.cpp new file mode 100644 index 0000000..74a6dff --- /dev/null +++ b/ultimmc/launcher/ui/widgets/PageContainer.cpp @@ -0,0 +1,240 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PageContainer.h" +#include "PageContainer_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "settings/SettingsObject.h" + +#include "ui/widgets/IconLabel.h" + +#include "DesktopServices.h" +#include "Application.h" + +class PageEntryFilterModel : public QSortFilterProxyModel +{ +public: + explicit PageEntryFilterModel(QObject *parent = 0) : QSortFilterProxyModel(parent) + { + } + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const + { + const QString pattern = filterRegExp().pattern(); + const auto model = static_cast(sourceModel()); + const auto page = model->pages().at(sourceRow); + if (!page->shouldDisplay()) + return false; + // Regular contents check, then check page-filter. + return QSortFilterProxyModel::filterAcceptsRow(sourceRow, sourceParent); + } +}; + +PageContainer::PageContainer(BasePageProvider *pageProvider, QString defaultId, + QWidget *parent) + : QWidget(parent) +{ + createUI(); + m_model = new PageModel(this); + m_proxyModel = new PageEntryFilterModel(this); + int counter = 0; + auto pages = pageProvider->getPages(); + for (auto page : pages) + { + page->stackIndex = m_pageStack->addWidget(dynamic_cast(page)); + page->listIndex = counter; + page->setParentContainer(this); + counter++; + } + m_model->setPages(pages); + + m_proxyModel->setSourceModel(m_model); + m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + + m_pageList->setIconSize(QSize(pageIconSize, pageIconSize)); + m_pageList->setSelectionMode(QAbstractItemView::SingleSelection); + m_pageList->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + m_pageList->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); + m_pageList->setModel(m_proxyModel); + connect(m_pageList->selectionModel(), SIGNAL(currentRowChanged(QModelIndex, QModelIndex)), + this, SLOT(currentChanged(QModelIndex))); + m_pageStack->setStackingMode(QStackedLayout::StackOne); + m_pageList->setFocus(); + selectPage(defaultId); +} + +bool PageContainer::selectPage(QString pageId) +{ + // now find what we want to have selected... + auto page = m_model->findPageEntryById(pageId); + QModelIndex index; + if (page) + { + index = m_proxyModel->mapFromSource(m_model->index(page->listIndex)); + } + if(!index.isValid()) + { + index = m_proxyModel->index(0, 0); + } + if (index.isValid()) + { + m_pageList->setCurrentIndex(index); + return true; + } + return false; +} + +void PageContainer::refreshContainer() +{ + m_proxyModel->invalidate(); + if(!m_currentPage->shouldDisplay()) + { + auto index = m_proxyModel->index(0, 0); + if(index.isValid()) + { + m_pageList->setCurrentIndex(index); + } + else + { + // FIXME: unhandled corner case: what to do when there's no page to select? + } + } +} + +void PageContainer::createUI() +{ + m_pageStack = new QStackedLayout; + m_pageList = new PageView; + m_header = new QLabel(); + m_iconHeader = new IconLabel(this, QIcon(), QSize(24, 24)); + + QFont headerLabelFont = m_header->font(); + headerLabelFont.setBold(true); + const int pointSize = headerLabelFont.pointSize(); + if (pointSize > 0) + headerLabelFont.setPointSize(pointSize + 2); + m_header->setFont(headerLabelFont); + + QHBoxLayout *headerHLayout = new QHBoxLayout; + const int leftMargin = APPLICATION->style()->pixelMetric(QStyle::PM_LayoutLeftMargin); + headerHLayout->addSpacerItem(new QSpacerItem(leftMargin, 0, QSizePolicy::Fixed, QSizePolicy::Ignored)); + headerHLayout->addWidget(m_header); + headerHLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Ignored)); + headerHLayout->addWidget(m_iconHeader); + const int rightMargin = APPLICATION->style()->pixelMetric(QStyle::PM_LayoutRightMargin); + headerHLayout->addSpacerItem(new QSpacerItem(rightMargin, 0, QSizePolicy::Fixed, QSizePolicy::Ignored)); + headerHLayout->setContentsMargins(0, 6, 0, 0); + + m_pageStack->setMargin(0); + m_pageStack->addWidget(new QWidget(this)); + + m_layout = new QGridLayout; + m_layout->addLayout(headerHLayout, 0, 1, 1, 1); + m_layout->addWidget(m_pageList, 0, 0, 2, 1); + m_layout->addLayout(m_pageStack, 1, 1, 1, 1); + m_layout->setColumnStretch(1, 4); + m_layout->setContentsMargins(0,0,0,6); + setLayout(m_layout); +} + +void PageContainer::addButtons(QWidget *buttons) +{ + m_layout->addWidget(buttons, 2, 0, 1, 2); +} + +void PageContainer::addButtons(QLayout *buttons) +{ + m_layout->addLayout(buttons, 2, 0, 1, 2); +} + +void PageContainer::showPage(int row) +{ + if (m_currentPage) + { + m_currentPage->closed(); + } + if (row != -1) + { + m_currentPage = m_model->pages().at(row); + } + else + { + m_currentPage = nullptr; + } + if (m_currentPage) + { + m_pageStack->setCurrentIndex(m_currentPage->stackIndex); + m_header->setText(m_currentPage->displayName()); + m_iconHeader->setIcon(m_currentPage->icon()); + m_currentPage->opened(); + } + else + { + m_pageStack->setCurrentIndex(0); + m_header->setText(QString()); + m_iconHeader->setIcon(APPLICATION->getThemedIcon("bug")); + } +} + +void PageContainer::help() +{ + if (m_currentPage) + { + QString pageId = m_currentPage->helpPage(); + if (pageId.isEmpty()) + return; + DesktopServices::openUrl(QUrl("https://github.com/MultiMC/Launcher/wiki/" + pageId)); + } +} + +void PageContainer::currentChanged(const QModelIndex ¤t) +{ + showPage(current.isValid() ? m_proxyModel->mapToSource(current).row() : -1); +} + +bool PageContainer::prepareToClose() +{ + if(!saveAll()) + { + return false; + } + if (m_currentPage) + { + m_currentPage->closed(); + } + return true; +} + +bool PageContainer::saveAll() +{ + for (auto page : m_model->pages()) + { + if (!page->apply()) + return false; + } + return true; +} diff --git a/ultimmc/launcher/ui/widgets/PageContainer.h b/ultimmc/launcher/ui/widgets/PageContainer.h new file mode 100644 index 0000000..8d2172d --- /dev/null +++ b/ultimmc/launcher/ui/widgets/PageContainer.h @@ -0,0 +1,89 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "ui/pages/BasePageProvider.h" +#include "ui/pages/BasePageContainer.h" + +class QLayout; +class IconLabel; +class QSortFilterProxyModel; +class PageModel; +class QLabel; +class QListView; +class QLineEdit; +class QStackedLayout; +class QGridLayout; + +class PageContainer : public QWidget, public BasePageContainer +{ + Q_OBJECT +public: + explicit PageContainer(BasePageProvider *pageProvider, QString defaultId = QString(), + QWidget *parent = 0); + virtual ~PageContainer() {} + + void addButtons(QWidget * buttons); + void addButtons(QLayout * buttons); + /* + * Save any unsaved state and prepare to be closed. + * @return true if everything can be saved, false if there is something that requires attention + */ + bool prepareToClose(); + bool saveAll(); + + /* request close - used by individual pages */ + bool requestClose() override + { + if(m_container) + { + return m_container->requestClose(); + } + return false; + } + + virtual bool selectPage(QString pageId) override; + + void refreshContainer() override; + virtual void setParentContainer(BasePageContainer * container) + { + m_container = container; + }; + +private: + void createUI(); + +public slots: + void help(); + +private slots: + void currentChanged(const QModelIndex ¤t); + void showPage(int row); + +private: + BasePageContainer * m_container = nullptr; + BasePage * m_currentPage = 0; + QSortFilterProxyModel *m_proxyModel; + PageModel *m_model; + QStackedLayout *m_pageStack; + QListView *m_pageList; + QLabel *m_header; + IconLabel *m_iconHeader; + QGridLayout *m_layout; +}; diff --git a/ultimmc/launcher/ui/widgets/PageContainer_p.h b/ultimmc/launcher/ui/widgets/PageContainer_p.h new file mode 100644 index 0000000..da1a66f --- /dev/null +++ b/ultimmc/launcher/ui/widgets/PageContainer_p.h @@ -0,0 +1,123 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +class BasePage; +const int pageIconSize = 24; + +class PageViewDelegate : public QStyledItemDelegate +{ +public: + PageViewDelegate(QObject *parent) : QStyledItemDelegate(parent) + { + } + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const + { + QSize size = QStyledItemDelegate::sizeHint(option, index); + size.setHeight(qMax(size.height(), 32)); + return size; + } +}; + +class PageModel : public QAbstractListModel +{ +public: + PageModel(QObject *parent = 0) : QAbstractListModel(parent) + { + QPixmap empty(pageIconSize, pageIconSize); + empty.fill(Qt::transparent); + m_emptyIcon = QIcon(empty); + } + virtual ~PageModel() {} + + int rowCount(const QModelIndex &parent = QModelIndex()) const + { + return parent.isValid() ? 0 : m_pages.size(); + } + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const + { + switch (role) + { + case Qt::DisplayRole: + return m_pages.at(index.row())->displayName(); + case Qt::DecorationRole: + { + QIcon icon = m_pages.at(index.row())->icon(); + if (icon.isNull()) + icon = m_emptyIcon; + // HACK: fixes icon stretching on windows. TODO: report Qt bug for this + return QIcon(icon.pixmap(QSize(48,48))); + } + } + return QVariant(); + } + + void setPages(const QList &pages) + { + beginResetModel(); + m_pages = pages; + endResetModel(); + } + const QList &pages() const + { + return m_pages; + } + + BasePage * findPageEntryById(QString id) + { + for(auto page: m_pages) + { + if (page->id() == id) + return page; + } + return nullptr; + } + + QList m_pages; + QIcon m_emptyIcon; +}; + +class PageView : public QListView +{ +public: + PageView(QWidget *parent = 0) : QListView(parent) + { + setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Expanding); + setItemDelegate(new PageViewDelegate(this)); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + } + + virtual QSize sizeHint() const + { + int width = sizeHintForColumn(0) + frameWidth() * 2 + 5; + if (verticalScrollBar()->isVisible()) + width += verticalScrollBar()->width(); + return QSize(width, 100); + } + + virtual bool eventFilter(QObject *obj, QEvent *event) + { + if (obj == verticalScrollBar() && + (event->type() == QEvent::Show || event->type() == QEvent::Hide)) + updateGeometry(); + return QListView::eventFilter(obj, event); + } +}; diff --git a/ultimmc/launcher/ui/widgets/ProgressWidget.cpp b/ultimmc/launcher/ui/widgets/ProgressWidget.cpp new file mode 100644 index 0000000..911e555 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/ProgressWidget.cpp @@ -0,0 +1,73 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#include "ProgressWidget.h" +#include +#include +#include +#include + +#include "tasks/Task.h" + +ProgressWidget::ProgressWidget(QWidget *parent) + : QWidget(parent) +{ + m_label = new QLabel(this); + m_label->setWordWrap(true); + m_bar = new QProgressBar(this); + m_bar->setMinimum(0); + m_bar->setMaximum(100); + QVBoxLayout *layout = new QVBoxLayout(this); + layout->addWidget(m_label); + layout->addWidget(m_bar); + layout->addStretch(); + setLayout(layout); +} + +void ProgressWidget::start(std::shared_ptr task) +{ + if (m_task) + { + disconnect(m_task.get(), 0, this, 0); + } + m_task = task; + connect(m_task.get(), &Task::finished, this, &ProgressWidget::handleTaskFinish); + connect(m_task.get(), &Task::status, this, &ProgressWidget::handleTaskStatus); + connect(m_task.get(), &Task::progress, this, &ProgressWidget::handleTaskProgress); + connect(m_task.get(), &Task::destroyed, this, &ProgressWidget::taskDestroyed); + if (!m_task->isRunning()) + { + QMetaObject::invokeMethod(m_task.get(), "start", Qt::QueuedConnection); + } +} +bool ProgressWidget::exec(std::shared_ptr task) +{ + QEventLoop loop; + connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); + start(task); + if (task->isRunning()) + { + loop.exec(); + } + return task->wasSuccessful(); +} + +void ProgressWidget::handleTaskFinish() +{ + if (!m_task->wasSuccessful()) + { + m_label->setText(m_task->failReason()); + } +} +void ProgressWidget::handleTaskStatus(const QString &status) +{ + m_label->setText(status); +} +void ProgressWidget::handleTaskProgress(qint64 current, qint64 total) +{ + m_bar->setMaximum(total); + m_bar->setValue(current); +} +void ProgressWidget::taskDestroyed() +{ + m_task = nullptr; +} diff --git a/ultimmc/launcher/ui/widgets/ProgressWidget.h b/ultimmc/launcher/ui/widgets/ProgressWidget.h new file mode 100644 index 0000000..fa67748 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/ProgressWidget.h @@ -0,0 +1,32 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#pragma once + +#include +#include + +class Task; +class QProgressBar; +class QLabel; + +class ProgressWidget : public QWidget +{ + Q_OBJECT +public: + explicit ProgressWidget(QWidget *parent = nullptr); + +public slots: + void start(std::shared_ptr task); + bool exec(std::shared_ptr task); + +private slots: + void handleTaskFinish(); + void handleTaskStatus(const QString &status); + void handleTaskProgress(qint64 current, qint64 total); + void taskDestroyed(); + +private: + QLabel *m_label; + QProgressBar *m_bar; + std::shared_ptr m_task; +}; diff --git a/ultimmc/launcher/ui/widgets/VersionListView.cpp b/ultimmc/launcher/ui/widgets/VersionListView.cpp new file mode 100644 index 0000000..aba0b1a --- /dev/null +++ b/ultimmc/launcher/ui/widgets/VersionListView.cpp @@ -0,0 +1,162 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include "VersionListView.h" + +VersionListView::VersionListView(QWidget *parent) + :QTreeView ( parent ) +{ + m_emptyString = tr("No versions are currently available."); +} + +void VersionListView::rowsInserted(const QModelIndex &parent, int start, int end) +{ + m_itemCount += end-start+1; + updateEmptyViewPort(); + QTreeView::rowsInserted(parent, start, end); +} + + +void VersionListView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) +{ + m_itemCount -= end-start+1; + updateEmptyViewPort(); + QTreeView::rowsInserted(parent, start, end); +} + +void VersionListView::setModel(QAbstractItemModel *model) +{ + m_itemCount = model->rowCount(); + updateEmptyViewPort(); + QTreeView::setModel(model); +} + +void VersionListView::reset() +{ + if(model()) + { + m_itemCount = model()->rowCount(); + } + else { + m_itemCount = 0; + } + updateEmptyViewPort(); + QTreeView::reset(); +} + +void VersionListView::setEmptyString(QString emptyString) +{ + m_emptyString = emptyString; + updateEmptyViewPort(); +} + +void VersionListView::setEmptyErrorString(QString emptyErrorString) +{ + m_emptyErrorString = emptyErrorString; + updateEmptyViewPort(); +} + +void VersionListView::setEmptyMode(VersionListView::EmptyMode mode) +{ + m_emptyMode = mode; + updateEmptyViewPort(); +} + +void VersionListView::updateEmptyViewPort() +{ +#ifndef QT_NO_ACCESSIBILITY + setAccessibleDescription(currentEmptyString()); +#endif /* !QT_NO_ACCESSIBILITY */ + + if(!m_itemCount) + { + viewport()->update(); + } +} + +void VersionListView::paintEvent(QPaintEvent *event) +{ + if(m_itemCount) + { + QTreeView::paintEvent(event); + } + else + { + paintInfoLabel(event); + } +} + +QString VersionListView::currentEmptyString() const +{ + if(m_itemCount) { + return QString(); + } + switch(m_emptyMode) + { + default: + case VersionListView::Empty: + return QString(); + case VersionListView::String: + return m_emptyString; + case VersionListView::ErrorString: + return m_emptyErrorString; + } +} + + +void VersionListView::paintInfoLabel(QPaintEvent *event) const +{ + QString emptyString = currentEmptyString(); + + //calculate the rect for the overlay + QPainter painter(viewport()); + painter.setRenderHint(QPainter::Antialiasing, true); + QFont font("sans", 20); + font.setBold(true); + + QRect bounds = viewport()->geometry(); + bounds.moveTop(0); + auto innerBounds = bounds; + innerBounds.adjust(10, 10, -10, -10); + + QColor background = QApplication::palette().color(QPalette::Foreground); + QColor foreground = QApplication::palette().color(QPalette::Base); + foreground.setAlpha(190); + painter.setFont(font); + auto fontMetrics = painter.fontMetrics(); + auto textRect = fontMetrics.boundingRect(innerBounds, Qt::AlignHCenter | Qt::TextWordWrap, emptyString); + textRect.moveCenter(bounds.center()); + + auto wrapRect = textRect; + wrapRect.adjust(-10, -10, 10, 10); + + //check if we are allowed to draw in our area + if (!event->rect().intersects(wrapRect)) { + return; + } + + painter.setBrush(QBrush(background)); + painter.setPen(foreground); + painter.drawRoundedRect(wrapRect, 5.0, 5.0); + + painter.setPen(foreground); + painter.setFont(font); + painter.drawText(textRect, Qt::AlignHCenter | Qt::TextWordWrap, emptyString); +} diff --git a/ultimmc/launcher/ui/widgets/VersionListView.h b/ultimmc/launcher/ui/widgets/VersionListView.h new file mode 100644 index 0000000..4153b31 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/VersionListView.h @@ -0,0 +1,56 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once +#include + +class VersionListView : public QTreeView +{ + Q_OBJECT +public: + + explicit VersionListView(QWidget *parent = 0); + virtual void paintEvent(QPaintEvent *event) override; + virtual void setModel(QAbstractItemModel* model) override; + + enum EmptyMode + { + Empty, + String, + ErrorString + }; + + void setEmptyString(QString emptyString); + void setEmptyErrorString(QString emptyErrorString); + void setEmptyMode(EmptyMode mode); + +public slots: + virtual void reset() override; + +protected slots: + virtual void rowsAboutToBeRemoved(const QModelIndex & parent, int start, int end) override; + virtual void rowsInserted(const QModelIndex &parent, int start, int end) override; + +private: /* methods */ + void paintInfoLabel(QPaintEvent *event) const; + void updateEmptyViewPort(); + QString currentEmptyString() const; + +private: /* variables */ + int m_itemCount = 0; + QString m_emptyString; + QString m_emptyErrorString; + EmptyMode m_emptyMode = Empty; +}; diff --git a/ultimmc/launcher/ui/widgets/VersionSelectWidget.cpp b/ultimmc/launcher/ui/widgets/VersionSelectWidget.cpp new file mode 100644 index 0000000..1209f11 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/VersionSelectWidget.cpp @@ -0,0 +1,205 @@ +#include "VersionSelectWidget.h" + +#include +#include +#include + +#include "VersionListView.h" +#include "VersionProxyModel.h" + +#include "ui/dialogs/CustomMessageBox.h" + +VersionSelectWidget::VersionSelectWidget(QWidget* parent) + : QWidget(parent) +{ + setObjectName(QStringLiteral("VersionSelectWidget")); + verticalLayout = new QVBoxLayout(this); + verticalLayout->setObjectName(QStringLiteral("verticalLayout")); + verticalLayout->setContentsMargins(0, 0, 0, 0); + + m_proxyModel = new VersionProxyModel(this); + + listView = new VersionListView(this); + listView->setObjectName(QStringLiteral("listView")); + listView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + listView->setAlternatingRowColors(true); + listView->setRootIsDecorated(false); + listView->setItemsExpandable(false); + listView->setWordWrap(true); + listView->header()->setCascadingSectionResizes(true); + listView->header()->setStretchLastSection(false); + listView->setModel(m_proxyModel); + verticalLayout->addWidget(listView); + + sneakyProgressBar = new QProgressBar(this); + sneakyProgressBar->setObjectName(QStringLiteral("sneakyProgressBar")); + sneakyProgressBar->setFormat(QStringLiteral("%p%")); + verticalLayout->addWidget(sneakyProgressBar); + sneakyProgressBar->setHidden(true); + connect(listView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &VersionSelectWidget::currentRowChanged); + + QMetaObject::connectSlotsByName(this); +} + +void VersionSelectWidget::setCurrentVersion(const QString& version) +{ + m_currentVersion = version; + m_proxyModel->setCurrentVersion(version); +} + +void VersionSelectWidget::setEmptyString(QString emptyString) +{ + listView->setEmptyString(emptyString); +} + +void VersionSelectWidget::setEmptyErrorString(QString emptyErrorString) +{ + listView->setEmptyErrorString(emptyErrorString); +} + +VersionSelectWidget::~VersionSelectWidget() +{ +} + +void VersionSelectWidget::setResizeOn(int column) +{ + listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::ResizeToContents); + resizeOnColumn = column; + listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch); +} + +void VersionSelectWidget::initialize(BaseVersionList *vlist) +{ + m_vlist = vlist; + m_proxyModel->setSourceModel(vlist); + listView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + listView->header()->setSectionResizeMode(resizeOnColumn, QHeaderView::Stretch); + + if (!m_vlist->isLoaded()) + { + loadList(); + } + else + { + if (m_proxyModel->rowCount() == 0) + { + listView->setEmptyMode(VersionListView::String); + } + preselect(); + } +} + +void VersionSelectWidget::closeEvent(QCloseEvent * event) +{ + QWidget::closeEvent(event); +} + +void VersionSelectWidget::loadList() +{ + auto newTask = m_vlist->getLoadTask(); + if (!newTask) + { + return; + } + loadTask = newTask.get(); + connect(loadTask, &Task::succeeded, this, &VersionSelectWidget::onTaskSucceeded); + connect(loadTask, &Task::failed, this, &VersionSelectWidget::onTaskFailed); + connect(loadTask, &Task::progress, this, &VersionSelectWidget::changeProgress); + if(!loadTask->isRunning()) + { + loadTask->start(); + } + sneakyProgressBar->setHidden(false); +} + +void VersionSelectWidget::onTaskSucceeded() +{ + if (m_proxyModel->rowCount() == 0) + { + listView->setEmptyMode(VersionListView::String); + } + sneakyProgressBar->setHidden(true); + preselect(); + loadTask = nullptr; +} + +void VersionSelectWidget::onTaskFailed(const QString& reason) +{ + CustomMessageBox::selectable(this, tr("Error"), tr("List update failed:\n%1").arg(reason), QMessageBox::Warning)->show(); + onTaskSucceeded(); +} + +void VersionSelectWidget::changeProgress(qint64 current, qint64 total) +{ + sneakyProgressBar->setMaximum(total); + sneakyProgressBar->setValue(current); +} + +void VersionSelectWidget::currentRowChanged(const QModelIndex& current, const QModelIndex&) +{ + auto variant = m_proxyModel->data(current, BaseVersionList::VersionPointerRole); + emit selectedVersionChanged(variant.value()); +} + +void VersionSelectWidget::preselect() +{ + if(preselectedAlready) + return; + selectCurrent(); + if(preselectedAlready) + return; + selectRecommended(); +} + +void VersionSelectWidget::selectCurrent() +{ + if(m_currentVersion.isEmpty()) + { + return; + } + auto idx = m_proxyModel->getVersion(m_currentVersion); + if(idx.isValid()) + { + preselectedAlready = true; + listView->selectionModel()->setCurrentIndex(idx,QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); + listView->scrollTo(idx, QAbstractItemView::PositionAtCenter); + } +} + +void VersionSelectWidget::selectRecommended() +{ + auto idx = m_proxyModel->getRecommended(); + if(idx.isValid()) + { + preselectedAlready = true; + listView->selectionModel()->setCurrentIndex(idx,QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); + listView->scrollTo(idx, QAbstractItemView::PositionAtCenter); + } +} + +bool VersionSelectWidget::hasVersions() const +{ + return m_proxyModel->rowCount(QModelIndex()) != 0; +} + +BaseVersionPtr VersionSelectWidget::selectedVersion() const +{ + auto currentIndex = listView->selectionModel()->currentIndex(); + auto variant = m_proxyModel->data(currentIndex, BaseVersionList::VersionPointerRole); + return variant.value(); +} + +void VersionSelectWidget::setExactFilter(BaseVersionList::ModelRoles role, QString filter) +{ + m_proxyModel->setFilter(role, new ExactFilter(filter)); +} + +void VersionSelectWidget::setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter) +{ + m_proxyModel->setFilter(role, new ContainsFilter(filter)); +} + +void VersionSelectWidget::setFilter(BaseVersionList::ModelRoles role, Filter *filter) +{ + m_proxyModel->setFilter(role, filter); +} diff --git a/ultimmc/launcher/ui/widgets/VersionSelectWidget.h b/ultimmc/launcher/ui/widgets/VersionSelectWidget.h new file mode 100644 index 0000000..0a64940 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/VersionSelectWidget.h @@ -0,0 +1,81 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include "BaseVersionList.h" + +class VersionProxyModel; +class VersionListView; +class QVBoxLayout; +class QProgressBar; +class Filter; + +class VersionSelectWidget: public QWidget +{ + Q_OBJECT +public: + explicit VersionSelectWidget(QWidget *parent = 0); + ~VersionSelectWidget(); + + //! loads the list if needed. + void initialize(BaseVersionList *vlist); + + //! Starts a task that loads the list. + void loadList(); + + bool hasVersions() const; + BaseVersionPtr selectedVersion() const; + void selectRecommended(); + void selectCurrent(); + + void setCurrentVersion(const QString & version); + void setFuzzyFilter(BaseVersionList::ModelRoles role, QString filter); + void setExactFilter(BaseVersionList::ModelRoles role, QString filter); + void setFilter(BaseVersionList::ModelRoles role, Filter *filter); + void setEmptyString(QString emptyString); + void setEmptyErrorString(QString emptyErrorString); + void setResizeOn(int column); + +signals: + void selectedVersionChanged(BaseVersionPtr version); + +protected: + virtual void closeEvent ( QCloseEvent* ); + +private slots: + void onTaskSucceeded(); + void onTaskFailed(const QString &reason); + void changeProgress(qint64 current, qint64 total); + void currentRowChanged(const QModelIndex ¤t, const QModelIndex &); + +private: + void preselect(); + +private: + QString m_currentVersion; + BaseVersionList *m_vlist = nullptr; + VersionProxyModel *m_proxyModel = nullptr; + int resizeOnColumn = 0; + Task * loadTask; + bool preselectedAlready = false; + +private: + QVBoxLayout *verticalLayout = nullptr; + VersionListView *listView = nullptr; + QProgressBar *sneakyProgressBar = nullptr; +}; diff --git a/ultimmc/launcher/ui/widgets/WideBar.cpp b/ultimmc/launcher/ui/widgets/WideBar.cpp new file mode 100644 index 0000000..8d01232 --- /dev/null +++ b/ultimmc/launcher/ui/widgets/WideBar.cpp @@ -0,0 +1,118 @@ +#include "WideBar.h" +#include +#include + +class ActionButton : public QToolButton +{ + Q_OBJECT +public: + ActionButton(QAction * action, QWidget * parent = 0) : QToolButton(parent), m_action(action) { + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + connect(action, &QAction::changed, this, &ActionButton::actionChanged); + connect(this, &ActionButton::clicked, action, &QAction::trigger); + actionChanged(); + }; +private slots: + void actionChanged() { + setEnabled(m_action->isEnabled()); + setChecked(m_action->isChecked()); + setCheckable(m_action->isCheckable()); + setText(m_action->text()); + setIcon(m_action->icon()); + setToolTip(m_action->toolTip()); + setFocusPolicy(Qt::NoFocus); + } +private: + QAction * m_action; +}; + + +WideBar::WideBar(const QString& title, QWidget* parent) : QToolBar(title, parent) +{ + setFloatable(false); + setMovable(false); +} + +WideBar::WideBar(QWidget* parent) : QToolBar(parent) +{ + setFloatable(false); + setMovable(false); +} + +struct WideBar::BarEntry { + enum Type { + None, + Action, + Separator, + Spacer + } type = None; + QAction *qAction = nullptr; + QAction *wideAction = nullptr; +}; + + +WideBar::~WideBar() +{ + for(auto *iter: m_entries) { + delete iter; + } +} + +void WideBar::addAction(QAction* action) +{ + auto entry = new BarEntry(); + entry->qAction = addWidget(new ActionButton(action, this)); + connect(action, &QAction::changed, entry->qAction, [entry, action](){ + entry->qAction->setVisible(action->isVisible()); + }); + entry->wideAction = action; + entry->type = BarEntry::Action; + m_entries.push_back(entry); +} + +void WideBar::addSeparator() +{ + auto entry = new BarEntry(); + entry->qAction = QToolBar::addSeparator(); + entry->type = BarEntry::Separator; + m_entries.push_back(entry); +} + +void WideBar::insertSpacer(QAction* action) +{ + auto iter = std::find_if(m_entries.begin(), m_entries.end(), [action](BarEntry * entry) { + return entry->wideAction == action; + }); + if(iter == m_entries.end()) { + return; + } + QWidget* spacer = new QWidget(); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + auto entry = new BarEntry(); + entry->qAction = insertWidget((*iter)->qAction, spacer); + entry->type = BarEntry::Spacer; + m_entries.insert(iter, entry); +} + +QMenu * WideBar::createContextMenu(QWidget *parent, const QString & title) +{ + QMenu *contextMenu = new QMenu(title, parent); + for(auto & item: m_entries) { + switch(item->type) { + default: + case BarEntry::None: + break; + case BarEntry::Separator: + case BarEntry::Spacer: + contextMenu->addSeparator(); + break; + case BarEntry::Action: + contextMenu->addAction(item->wideAction); + break; + } + } + return contextMenu; +} + +#include "WideBar.moc" diff --git a/ultimmc/launcher/ui/widgets/WideBar.h b/ultimmc/launcher/ui/widgets/WideBar.h new file mode 100644 index 0000000..d1b8cbe --- /dev/null +++ b/ultimmc/launcher/ui/widgets/WideBar.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +class QMenu; + +class WideBar : public QToolBar +{ + Q_OBJECT + +public: + explicit WideBar(const QString &title, QWidget * parent = nullptr); + explicit WideBar(QWidget * parent = nullptr); + virtual ~WideBar(); + + void addAction(QAction *action); + void addSeparator(); + void insertSpacer(QAction *action); + QMenu *createContextMenu(QWidget *parent = nullptr, const QString & title = QString()); + +private: + struct BarEntry; + QList m_entries; +}; diff --git a/ultimmc/launcher/updater/DownloadTask.cpp b/ultimmc/launcher/updater/DownloadTask.cpp new file mode 100644 index 0000000..48fe767 --- /dev/null +++ b/ultimmc/launcher/updater/DownloadTask.cpp @@ -0,0 +1,177 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "DownloadTask.h" + +#include "updater/UpdateChecker.h" +#include "GoUpdate.h" +#include "net/NetJob.h" + +#include +#include +#include + +namespace GoUpdate +{ + +DownloadTask::DownloadTask( + shared_qobject_ptr network, + Status status, + QString target, + QObject *parent +) : Task(parent), m_updateFilesDir(target), m_network(network) +{ + m_status = status; + + m_updateFilesDir.setAutoRemove(false); +} + +void DownloadTask::executeTask() +{ + loadVersionInfo(); +} + +void DownloadTask::loadVersionInfo() +{ + setStatus(tr("Loading version information...")); + + NetJob *netJob = new NetJob("Version Info", m_network); + + // Find the index URL. + QUrl newIndexUrl = QUrl(m_status.newRepoUrl).resolved(QString::number(m_status.newVersionId) + ".json"); + qDebug() << m_status.newRepoUrl << " turns into " << newIndexUrl; + + netJob->addNetAction(m_newVersionFileListDownload = Net::Download::makeByteArray(newIndexUrl, &newVersionFileListData)); + + // If we have a current version URL, get that one too. + if (!m_status.currentRepoUrl.isEmpty()) + { + QUrl cIndexUrl = QUrl(m_status.currentRepoUrl).resolved(QString::number(m_status.currentVersionId) + ".json"); + netJob->addNetAction(m_currentVersionFileListDownload = Net::Download::makeByteArray(cIndexUrl, ¤tVersionFileListData)); + qDebug() << m_status.currentRepoUrl << " turns into " << cIndexUrl; + } + + // connect signals and start the job + connect(netJob, &NetJob::succeeded, this, &DownloadTask::processDownloadedVersionInfo); + connect(netJob, &NetJob::failed, this, &DownloadTask::vinfoDownloadFailed); + m_vinfoNetJob.reset(netJob); + netJob->start(); +} + +void DownloadTask::vinfoDownloadFailed() +{ + // Something failed. We really need the second download (current version info), so parse + // downloads anyways as long as the first one succeeded. + if (m_newVersionFileListDownload->wasSuccessful()) + { + processDownloadedVersionInfo(); + return; + } + + // TODO: Give a more detailed error message. + qCritical() << "Failed to download version info files."; + emitFailed(tr("Failed to download version info files.")); +} + +void DownloadTask::processDownloadedVersionInfo() +{ + VersionFileList m_currentVersionFileList; + VersionFileList m_newVersionFileList; + + setStatus(tr("Reading file list for new version...")); + qDebug() << "Reading file list for new version..."; + QString error; + if (!parseVersionInfo(newVersionFileListData, m_newVersionFileList, error)) + { + qCritical() << error; + emitFailed(error); + return; + } + + // if we have the current version info, use it. + if (m_currentVersionFileListDownload && m_currentVersionFileListDownload->wasSuccessful()) + { + setStatus(tr("Reading file list for current version...")); + qDebug() << "Reading file list for current version..."; + // if this fails, it's not a complete loss. + QString error; + if(!parseVersionInfo( currentVersionFileListData, m_currentVersionFileList, error)) + { + qDebug() << error << "This is not a fatal error."; + } + } + + // We don't need this any more. + m_currentVersionFileListDownload.reset(); + m_newVersionFileListDownload.reset(); + m_vinfoNetJob.reset(); + + setStatus(tr("Processing file lists - figuring out how to install the update...")); + + // make a new netjob for the actual update files + NetJob::Ptr netJob = new NetJob("Update Files", m_network); + + // fill netJob and operationList + if (!processFileLists(m_currentVersionFileList, m_newVersionFileList, m_status.rootPath, m_updateFilesDir.path(), netJob, m_operations)) + { + emitFailed(tr("Failed to process update lists...")); + return; + } + + // Now start the download. + QObject::connect(netJob.get(), &NetJob::succeeded, this, &DownloadTask::fileDownloadFinished); + QObject::connect(netJob.get(), &NetJob::progress, this, &DownloadTask::fileDownloadProgressChanged); + QObject::connect(netJob.get(), &NetJob::failed, this, &DownloadTask::fileDownloadFailed); + + if(netJob->size() == 1) // Translation issues... see https://github.com/MultiMC/Launcher/issues/1701 + { + setStatus(tr("Downloading one update file.")); + } + else + { + setStatus(tr("Downloading %1 update files.").arg(QString::number(netJob->size()))); + } + qDebug() << "Begin downloading update files to" << m_updateFilesDir.path(); + m_filesNetJob = netJob; + m_filesNetJob->start(); +} + +void DownloadTask::fileDownloadFinished() +{ + emitSucceeded(); +} + +void DownloadTask::fileDownloadFailed(QString reason) +{ + qCritical() << "Failed to download update files:" << reason; + emitFailed(tr("Failed to download update files: %1").arg(reason)); +} + +void DownloadTask::fileDownloadProgressChanged(qint64 current, qint64 total) +{ + setProgress(current, total); +} + +QString DownloadTask::updateFilesDir() +{ + return m_updateFilesDir.path(); +} + +OperationList DownloadTask::operations() +{ + return m_operations; +} + +} diff --git a/ultimmc/launcher/updater/DownloadTask.h b/ultimmc/launcher/updater/DownloadTask.h new file mode 100644 index 0000000..eac2623 --- /dev/null +++ b/ultimmc/launcher/updater/DownloadTask.h @@ -0,0 +1,99 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "tasks/Task.h" +#include "net/NetJob.h" +#include "GoUpdate.h" + +namespace GoUpdate +{ +/*! + * The DownloadTask is a task that takes a given version ID and repository URL, + * downloads that version's files from the repository, and prepares to install them. + */ +class DownloadTask : public Task +{ + Q_OBJECT + +public: + /** + * Create a download task + * + * target is a template - XXXXXX at the end will be replaced with a random generated string, ensuring uniqueness + */ + explicit DownloadTask(shared_qobject_ptr network, Status status, QString target, QObject* parent = 0); + virtual ~DownloadTask() {}; + + /// Get the directory that will contain the update files. + QString updateFilesDir(); + + /// Get the list of operations that should be done + OperationList operations(); + + /// set updater download behavior + void setUseLocalUpdater(bool useLocal); + +protected: + //! Entry point for tasks. + virtual void executeTask() override; + + /*! + * Downloads the version info files from the repository. + * The files for both the current build, and the build that we're updating to need to be downloaded. + * If the current version's info file can't be found, MultiMC will not delete files that + * were removed between versions. It will still replace files that have changed, however. + * Note that although the repository URL for the current version is not given to the update task, + * the task will attempt to look it up in the UpdateChecker's channel list. + * If an error occurs here, the function will call emitFailed and return false. + */ + void loadVersionInfo(); + + NetJob::Ptr m_vinfoNetJob; + QByteArray currentVersionFileListData; + QByteArray newVersionFileListData; + Net::Download::Ptr m_currentVersionFileListDownload; + Net::Download::Ptr m_newVersionFileListDownload; + + NetJob::Ptr m_filesNetJob; + + Status m_status; + + OperationList m_operations; + + /*! + * Temporary directory to store update files in. + * This will be set to not auto delete. Task will fail if this fails to be created. + */ + QTemporaryDir m_updateFilesDir; + +protected slots: + /*! + * This function is called when version information is finished downloading + * and at least the new file list download succeeded + */ + void processDownloadedVersionInfo(); + void vinfoDownloadFailed(); + + void fileDownloadFinished(); + void fileDownloadFailed(QString reason); + void fileDownloadProgressChanged(qint64 current, qint64 total); + +private: + shared_qobject_ptr m_network; +}; + +} diff --git a/ultimmc/launcher/updater/DownloadTask_test.cpp b/ultimmc/launcher/updater/DownloadTask_test.cpp new file mode 100644 index 0000000..8e823a6 --- /dev/null +++ b/ultimmc/launcher/updater/DownloadTask_test.cpp @@ -0,0 +1,196 @@ +#include +#include + +#include "TestUtil.h" + +#include "updater/GoUpdate.h" +#include "updater/DownloadTask.h" +#include "updater/UpdateChecker.h" +#include + +using namespace GoUpdate; + +FileSourceList encodeBaseFile(const char *suffix) +{ + auto base = QDir::currentPath(); + QUrl localFile = QUrl::fromLocalFile(base + suffix); + QString localUrlString = localFile.toString(QUrl::FullyEncoded); + auto item = FileSource("http", localUrlString); + return FileSourceList({item}); +} + +Q_DECLARE_METATYPE(VersionFileList) +Q_DECLARE_METATYPE(Operation) + +QDebug operator<<(QDebug dbg, const FileSource &f) +{ + dbg.nospace() << "FileSource(type=" << f.type << " url=" << f.url + << " comp=" << f.compressionType << ")"; + return dbg.maybeSpace(); +} + +QDebug operator<<(QDebug dbg, const VersionFileEntry &v) +{ + dbg.nospace() << "VersionFileEntry(path=" << v.path << " mode=" << v.mode + << " md5=" << v.md5 << " sources=" << v.sources << ")"; + return dbg.maybeSpace(); +} + +QDebug operator<<(QDebug dbg, const Operation::Type &t) +{ + switch (t) + { + case Operation::OP_REPLACE: + dbg << "OP_COPY"; + break; + case Operation::OP_DELETE: + dbg << "OP_DELETE"; + break; + } + return dbg.maybeSpace(); +} + +QDebug operator<<(QDebug dbg, const Operation &u) +{ + dbg.nospace() << "Operation(type=" << u.type << " file=" << u.source + << " dest=" << u.destination << " mode=" << u.destinationMode << ")"; + return dbg.maybeSpace(); +} + +class DownloadTaskTest : public QObject +{ + Q_OBJECT +private +slots: + void initTestCase() + { + } + void cleanupTestCase() + { + } + + void test_parseVersionInfo_data() + { + QTest::addColumn("data"); + QTest::addColumn("list"); + QTest::addColumn("error"); + QTest::addColumn("ret"); + + QTest::newRow("one") + << GET_TEST_FILE("data/1.json") + << (VersionFileList() + << VersionFileEntry{"fileOne", + 493, + encodeBaseFile("/data/fileOneA"), + "9eb84090956c484e32cb6c08455a667b"} + << VersionFileEntry{"fileTwo", + 644, + encodeBaseFile("/data/fileTwo"), + "38f94f54fa3eb72b0ea836538c10b043"} + << VersionFileEntry{"fileThree", + 750, + encodeBaseFile("/data/fileThree"), + "f12df554b21e320be6471d7154130e70"}) + << QString() << true; + QTest::newRow("two") + << GET_TEST_FILE("data/2.json") + << (VersionFileList() + << VersionFileEntry{"fileOne", + 493, + encodeBaseFile("/data/fileOneB"), + "42915a71277c9016668cce7b82c6b577"} + << VersionFileEntry{"fileTwo", + 644, + encodeBaseFile("/data/fileTwo"), + "38f94f54fa3eb72b0ea836538c10b043"}) + << QString() << true; + } + void test_parseVersionInfo() + { + QFETCH(QByteArray, data); + QFETCH(VersionFileList, list); + QFETCH(QString, error); + QFETCH(bool, ret); + + VersionFileList outList; + QString outError; + bool outRet = parseVersionInfo(data, outList, outError); + QCOMPARE(outRet, ret); + QCOMPARE(outList, list); + QCOMPARE(outError, error); + } + + void test_processFileLists_data() + { + QTest::addColumn("tempFolder"); + QTest::addColumn("currentVersion"); + QTest::addColumn("newVersion"); + QTest::addColumn("expectedOperations"); + + QTemporaryDir tempFolderObj; + QString tempFolder = tempFolderObj.path(); + // update fileOne, keep fileTwo, remove fileThree + QTest::newRow("test 1") + << tempFolder << (VersionFileList() + << VersionFileEntry{ + "data/fileOne", 493, + FileSourceList() + << FileSource( + "http", "http://host/path/fileOne-1"), + "9eb84090956c484e32cb6c08455a667b"} + << VersionFileEntry{ + "data/fileTwo", 644, + FileSourceList() + << FileSource( + "http", "http://host/path/fileTwo-1"), + "38f94f54fa3eb72b0ea836538c10b043"} + << VersionFileEntry{ + "data/fileThree", 420, + FileSourceList() + << FileSource( + "http", "http://host/path/fileThree-1"), + "f12df554b21e320be6471d7154130e70"}) + << (VersionFileList() + << VersionFileEntry{ + "data/fileOne", 493, + FileSourceList() + << FileSource("http", + "http://host/path/fileOne-2"), + "42915a71277c9016668cce7b82c6b577"} + << VersionFileEntry{ + "data/fileTwo", 644, + FileSourceList() + << FileSource("http", + "http://host/path/fileTwo-2"), + "38f94f54fa3eb72b0ea836538c10b043"}) + << (OperationList() + << Operation::DeleteOp("data/fileThree") + << Operation::CopyOp( + FS::PathCombine(tempFolder, + QString("data/fileOne").replace("/", "_")), + "data/fileOne", 493)); + } + void test_processFileLists() + { + QFETCH(QString, tempFolder); + QFETCH(VersionFileList, currentVersion); + QFETCH(VersionFileList, newVersion); + QFETCH(OperationList, expectedOperations); + + OperationList operations; + + shared_qobject_ptr network = new QNetworkAccessManager(); + processFileLists(currentVersion, newVersion, QDir::currentPath(), tempFolder, new NetJob("Dummy", network), operations); + qDebug() << (operations == expectedOperations); + qDebug() << operations; + qDebug() << expectedOperations; + QCOMPARE(operations, expectedOperations); + } +}; + +extern "C" +{ + QTEST_GUILESS_MAIN(DownloadTaskTest) +} + +#include "DownloadTask_test.moc" diff --git a/ultimmc/launcher/updater/GoUpdate.cpp b/ultimmc/launcher/updater/GoUpdate.cpp new file mode 100644 index 0000000..76f68b5 --- /dev/null +++ b/ultimmc/launcher/updater/GoUpdate.cpp @@ -0,0 +1,198 @@ +#include "GoUpdate.h" +#include +#include +#include +#include + +#include "net/Download.h" +#include "net/ChecksumValidator.h" + +namespace GoUpdate +{ + +bool parseVersionInfo(const QByteArray &data, VersionFileList &list, QString &error) +{ + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + error = QString("Failed to parse version info JSON: %1 at %2") + .arg(jsonError.errorString()) + .arg(jsonError.offset); + qCritical() << error; + return false; + } + + QJsonObject json = jsonDoc.object(); + + qDebug() << data; + qDebug() << "Loading version info from JSON."; + QJsonArray filesArray = json.value("Files").toArray(); + for (QJsonValue fileValue : filesArray) + { + QJsonObject fileObj = fileValue.toObject(); + + QString file_path = fileObj.value("Path").toString(); + + VersionFileEntry file{file_path, fileObj.value("Perms").toVariant().toInt(), + FileSourceList(), fileObj.value("MD5").toString(), }; + qDebug() << "File" << file.path << "with perms" << file.mode; + + QJsonArray sourceArray = fileObj.value("Sources").toArray(); + for (QJsonValue val : sourceArray) + { + QJsonObject sourceObj = val.toObject(); + + QString type = sourceObj.value("SourceType").toString(); + if (type == "http") + { + file.sources.append(FileSource("http", sourceObj.value("Url").toString())); + } + else + { + qWarning() << "Unknown source type" << type << "ignored."; + } + } + + qDebug() << "Loaded info for" << file.path; + + list.append(file); + } + + return true; +} + +bool processFileLists +( + const VersionFileList ¤tVersion, + const VersionFileList &newVersion, + const QString &rootPath, + const QString &tempPath, + NetJob::Ptr job, + OperationList &ops +) +{ + // First, if we've loaded the current version's file list, we need to iterate through it and + // delete anything in the current one version's list that isn't in the new version's list. + for (VersionFileEntry entry : currentVersion) + { + QFileInfo toDelete(FS::PathCombine(rootPath, entry.path)); + if (!toDelete.exists()) + { + qCritical() << "Expected file " << toDelete.absoluteFilePath() + << " doesn't exist!"; + } + bool keep = false; + + // + for (VersionFileEntry newEntry : newVersion) + { + if (newEntry.path == entry.path) + { + qDebug() << "Not deleting" << entry.path + << "because it is still present in the new version."; + keep = true; + break; + } + } + + // If the loop reaches the end and we didn't find a match, delete the file. + if (!keep) + { + if (toDelete.exists()) + ops.append(Operation::DeleteOp(entry.path)); + } + } + + // Next, check each file in MultiMC's folder and see if we need to update them. + for (VersionFileEntry entry : newVersion) + { + // TODO: Let's not MD5sum a ton of files on the GUI thread. We should probably find a + // way to do this in the background. + QString fileMD5; + QString realEntryPath = FS::PathCombine(rootPath, entry.path); + QFile entryFile(realEntryPath); + QFileInfo entryInfo(realEntryPath); + + bool needs_upgrade = false; + if (!entryFile.exists()) + { + needs_upgrade = true; + } + else + { + bool pass = true; + if (!entryInfo.isReadable()) + { + qCritical() << "File " << realEntryPath << " is not readable."; + pass = false; + } + if (!entryInfo.isWritable()) + { + qCritical() << "File " << realEntryPath << " is not writable."; + pass = false; + } + if (!entryFile.open(QFile::ReadOnly)) + { + qCritical() << "File " << realEntryPath << " cannot be opened for reading."; + pass = false; + } + if (!pass) + { + ops.clear(); + return false; + } + } + + if(!needs_upgrade) + { + QCryptographicHash hash(QCryptographicHash::Md5); + auto foo = entryFile.readAll(); + + hash.addData(foo); + fileMD5 = hash.result().toHex(); + if ((fileMD5 != entry.md5)) + { + qDebug() << "MD5Sum does not match!"; + qDebug() << "Expected:'" << entry.md5 << "'"; + qDebug() << "Got: '" << fileMD5 << "'"; + needs_upgrade = true; + } + } + + // skip file. it doesn't need an upgrade. + if (!needs_upgrade) + { + qDebug() << "File" << realEntryPath << " does not need updating."; + continue; + } + + // yep. this file actually needs an upgrade. PROCEED. + qDebug() << "Found file" << realEntryPath << " that needs updating."; + + // Go through the sources list and find one to use. + // TODO: Make a NetAction that takes a source list and tries each of them until one + // works. For now, we'll just use the first http one. + for (FileSource source : entry.sources) + { + if (source.type != "http") + continue; + + qDebug() << "Will download" << entry.path << "from" << source.url; + + // Download it to updatedir/- where filepath is the file's + // path with slashes replaced by underscores. + QString dlPath = FS::PathCombine(tempPath, QString(entry.path).replace("/", "_")); + + // We need to download the file to the updatefiles folder and add a task + // to copy it to its install path. + auto download = Net::Download::makeFile(source.url, dlPath); + auto rawMd5 = QByteArray::fromHex(entry.md5.toLatin1()); + download->addValidator(new Net::ChecksumValidator(QCryptographicHash::Md5, rawMd5)); + job->addNetAction(download); + ops.append(Operation::CopyOp(dlPath, entry.path, entry.mode)); + } + } + return true; +} +} diff --git a/ultimmc/launcher/updater/GoUpdate.h b/ultimmc/launcher/updater/GoUpdate.h new file mode 100644 index 0000000..46a679e --- /dev/null +++ b/ultimmc/launcher/updater/GoUpdate.h @@ -0,0 +1,125 @@ +#pragma once +#include +#include + +namespace GoUpdate +{ + +/** + * A temporary object exchanged between updated checker and the actual update task + */ +struct Status +{ + bool updateAvailable = false; + + int newVersionId = -1; + QString newRepoUrl; + + int currentVersionId = -1; + QString currentRepoUrl; + + // path to the root of the application + QString rootPath; +}; + +/** + * Struct that describes an entry in a VersionFileEntry's `Sources` list. + */ +struct FileSource +{ + FileSource(QString type, QString url, QString compression="") + { + this->type = type; + this->url = url; + this->compressionType = compression; + } + + bool operator==(const FileSource &f2) const + { + return type == f2.type && url == f2.url && compressionType == f2.compressionType; + } + + QString type; + QString url; + QString compressionType; +}; +typedef QList FileSourceList; + +/** + * Structure that describes an entry in a GoUpdate version's `Files` list. + */ +struct VersionFileEntry +{ + QString path; + int mode; + FileSourceList sources; + QString md5; + bool operator==(const VersionFileEntry &v2) const + { + return path == v2.path && mode == v2.mode && sources == v2.sources && md5 == v2.md5; + } +}; +typedef QList VersionFileList; + +/** + * Structure that describes an operation to perform when installing updates. + */ +struct Operation +{ + static Operation CopyOp(QString from, QString to, int fmode=0644) + { + return Operation{OP_REPLACE, from, to, fmode}; + } + static Operation DeleteOp(QString file) + { + return Operation{OP_DELETE, QString(), file, 0644}; + } + + // FIXME: for some types, some of the other fields are irrelevant! + bool operator==(const Operation &u2) const + { + return type == u2.type && + source == u2.source && + destination == u2.destination && + destinationMode == u2.destinationMode; + } + + //! Specifies the type of operation that this is. + enum Type + { + OP_REPLACE, + OP_DELETE, + } type; + + //! The source file, if any + QString source; + + //! The destination file. + QString destination; + + //! The mode to change the destination file to. + int destinationMode; +}; +typedef QList OperationList; + +/** + * Loads the file list from the given version info JSON object into the given list. + */ +bool parseVersionInfo(const QByteArray &data, VersionFileList& list, QString &error); + +/*! + * Takes a list of file entries for the current version's files and the new version's files + * and populates the downloadList and operationList with information about how to download and install the update. + */ +bool processFileLists +( + const VersionFileList ¤tVersion, + const VersionFileList &newVersion, + const QString &rootPath, + const QString &tempPath, + NetJob::Ptr job, + OperationList &ops +); + +} +Q_DECLARE_METATYPE(GoUpdate::Status) diff --git a/ultimmc/launcher/updater/UpdateChecker.cpp b/ultimmc/launcher/updater/UpdateChecker.cpp new file mode 100644 index 0000000..c050590 --- /dev/null +++ b/ultimmc/launcher/updater/UpdateChecker.cpp @@ -0,0 +1,270 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "UpdateChecker.h" + +#include +#include +#include +#include + +#define API_VERSION 0 +#define CHANLIST_FORMAT 0 + +#include "BuildConfig.h" +#include "sys.h" + +UpdateChecker::UpdateChecker(shared_qobject_ptr nam, QString channelUrl, int currentBuild) +{ + m_network = nam; + m_channelUrl = channelUrl; + m_currentChannel = "develop"; + m_currentBuild = currentBuild; +} + +QList UpdateChecker::getChannelList() const +{ + return m_channels; +} + +bool UpdateChecker::hasChannels() const +{ + return !m_channels.isEmpty(); +} + +void UpdateChecker::checkForUpdate(bool notifyNoUpdate) +{ + qDebug() << "Checking for updates."; + QString updateChannel = "develop"; + + // If the channel list hasn't loaded yet, load it and defer checking for updates until + // later. + if (!m_chanListLoaded) + { + qDebug() << "Channel list isn't loaded yet. Loading channel list and deferring update check."; + m_checkUpdateWaiting = true; + updateChanList(notifyNoUpdate); + return; + } + + if (m_updateChecking) + { + qDebug() << "Ignoring update check request. Already checking for updates."; + return; + } + + // Find the desired channel within the channel list and get its repo URL. If if cannot be + // found, error. + QString developUrl; + m_newRepoUrl = ""; + for (ChannelListEntry entry : m_channels) + { + qDebug() << "channelEntry = " << entry.id; + if(entry.id == "develop") { + developUrl = entry.url; + } + if (entry.id == updateChannel) { + m_newRepoUrl = entry.url; + qDebug() << "is intended update channel: " << entry.id; + } + if (entry.id == m_currentChannel) { + m_currentRepoUrl = entry.url; + qDebug() << "is current update channel: " << entry.id; + } + } + + qDebug() << "m_repoUrl = " << m_newRepoUrl; + + if (m_newRepoUrl.isEmpty()) { + qWarning() << "m_repoUrl was empty. defaulting to 'develop': " << developUrl; + m_newRepoUrl = developUrl; + } + + // If nothing applies, error + if (m_newRepoUrl.isEmpty()) + { + qCritical() << "failed to select any update repository for: " << updateChannel; + emit updateCheckFailed(); + return; + } + + m_updateChecking = true; + + QUrl indexUrl = QUrl(m_newRepoUrl).resolved(QUrl("index.json")); + + indexJob = new NetJob("GoUpdate Repository Index", m_network); + indexJob->addNetAction(Net::Download::makeByteArray(indexUrl, &indexData)); + connect(indexJob.get(), &NetJob::succeeded, [this, notifyNoUpdate](){ updateCheckFinished(notifyNoUpdate); }); + connect(indexJob.get(), &NetJob::failed, this, &UpdateChecker::updateCheckFailed); + indexJob->start(); +} + +void UpdateChecker::updateCheckFinished(bool notifyNoUpdate) +{ + qDebug() << "Finished downloading repo index. Checking for new versions."; + + QJsonParseError jsonError; + indexJob.reset(); + + QJsonDocument jsonDoc = QJsonDocument::fromJson(indexData, &jsonError); + indexData.clear(); + if (jsonError.error != QJsonParseError::NoError || !jsonDoc.isObject()) + { + qCritical() << "Failed to parse GoUpdate repository index. JSON error" + << jsonError.errorString() << "at offset" << jsonError.offset; + m_updateChecking = false; + return; + } + + QJsonObject object = jsonDoc.object(); + + bool success = false; + int apiVersion = object.value("ApiVersion").toVariant().toInt(&success); + if (apiVersion != API_VERSION || !success) + { + qCritical() << "Failed to check for updates. API version mismatch. We're using" + << API_VERSION << "server has" << apiVersion; + m_updateChecking = false; + return; + } + + qDebug() << "Processing repository version list."; + QJsonObject newestVersion; + QJsonArray versions = object.value("Versions").toArray(); + for (QJsonValue versionVal : versions) + { + QJsonObject version = versionVal.toObject(); + if (newestVersion.value("Id").toVariant().toInt() < + version.value("Id").toVariant().toInt()) + { + newestVersion = version; + } + } + + // We've got the version with the greatest ID number. Now compare it to our current build + // number and update if they're different. + int newBuildNumber = newestVersion.value("Id").toVariant().toInt(); + if (newBuildNumber != m_currentBuild) + { + qDebug() << "Found newer version with ID" << newBuildNumber; + // Update! + GoUpdate::Status updateStatus; + updateStatus.updateAvailable = true; + updateStatus.currentVersionId = m_currentBuild; + updateStatus.currentRepoUrl = m_currentRepoUrl; + updateStatus.newVersionId = newBuildNumber; + updateStatus.newRepoUrl = m_newRepoUrl; + emit updateAvailable(updateStatus); + } + else if (notifyNoUpdate) + { + emit noUpdateFound(); + } + m_updateChecking = false; +} + +void UpdateChecker::updateCheckFailed() +{ + qCritical() << "Update check failed for reasons unknown."; +} + +void UpdateChecker::updateChanList(bool notifyNoUpdate) +{ + qDebug() << "Loading the channel list."; + + if (m_chanListLoading) + { + qDebug() << "Ignoring channel list update request. Already grabbing channel list."; + return; + } + + m_chanListLoading = true; + chanListJob = new NetJob("Update System Channel List", m_network); + chanListJob->addNetAction(Net::Download::makeByteArray(QUrl(m_channelUrl), &chanlistData)); + connect(chanListJob.get(), &NetJob::succeeded, [this, notifyNoUpdate]() { chanListDownloadFinished(notifyNoUpdate); }); + connect(chanListJob.get(), &NetJob::failed, this, &UpdateChecker::chanListDownloadFailed); + chanListJob->start(); +} + +void UpdateChecker::chanListDownloadFinished(bool notifyNoUpdate) +{ + chanListJob.reset(); + + QJsonParseError jsonError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(chanlistData, &jsonError); + chanlistData.clear(); + if (jsonError.error != QJsonParseError::NoError) + { + // TODO: Report errors to the user. + qCritical() << "Failed to parse channel list JSON:" << jsonError.errorString() << "at" << jsonError.offset; + m_chanListLoading = false; + return; + } + + QJsonObject object = jsonDoc.object(); + + bool success = false; + int formatVersion = object.value("format_version").toVariant().toInt(&success); + if (formatVersion != CHANLIST_FORMAT || !success) + { + qCritical() + << "Failed to check for updates. Channel list format version mismatch. We're using" + << CHANLIST_FORMAT << "server has" << formatVersion; + m_chanListLoading = false; + return; + } + + // Load channels into a temporary array. + QList loadedChannels; + QJsonArray channelArray = object.value("channels").toArray(); + for (QJsonValue chanVal : channelArray) + { + QJsonObject channelObj = chanVal.toObject(); + ChannelListEntry entry { + channelObj.value("id").toVariant().toString(), + channelObj.value("name").toVariant().toString(), + channelObj.value("description").toVariant().toString(), + channelObj.value("url").toVariant().toString() + }; + if (entry.id.isEmpty() || entry.name.isEmpty() || entry.url.isEmpty()) + { + qCritical() << "Channel list entry with empty ID, name, or URL. Skipping."; + continue; + } + loadedChannels.append(entry); + } + + // Swap the channel list we just loaded into the object's channel list. + m_channels.swap(loadedChannels); + + m_chanListLoading = false; + m_chanListLoaded = true; + qDebug() << "Successfully loaded UpdateChecker channel list."; + + // If we're waiting to check for updates, do that now. + if (m_checkUpdateWaiting) { + checkForUpdate(notifyNoUpdate); + } + + emit channelListLoaded(); +} + +void UpdateChecker::chanListDownloadFailed(QString reason) +{ + m_chanListLoading = false; + qCritical() << QString("Failed to download channel list: %1").arg(reason); + emit channelListLoaded(); +} + diff --git a/ultimmc/launcher/updater/UpdateChecker.h b/ultimmc/launcher/updater/UpdateChecker.h new file mode 100644 index 0000000..6fe4180 --- /dev/null +++ b/ultimmc/launcher/updater/UpdateChecker.h @@ -0,0 +1,116 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "net/NetJob.h" +#include "GoUpdate.h" + +class UpdateChecker : public QObject +{ + Q_OBJECT + +public: + UpdateChecker(shared_qobject_ptr nam, QString channelUrl, int currentBuild); + void checkForUpdate(bool notifyNoUpdate); + + /*! + * Causes the update checker to download the channel list from the URL specified in config.h (generated by CMake). + * If this isn't called before checkForUpdate(), it will automatically be called. + */ + void updateChanList(bool notifyNoUpdate); + + /*! + * An entry in the channel list. + */ + struct ChannelListEntry + { + QString id; + QString name; + QString description; + QString url; + }; + + /*! + * Returns a the current channel list. + * If the channel list hasn't been loaded, this list will be empty. + */ + QList getChannelList() const; + + /*! + * Returns false if the channel list is empty. + */ + bool hasChannels() const; + +signals: + //! Signal emitted when an update is available. Passes the URL for the repo and the ID and name for the version. + void updateAvailable(GoUpdate::Status status); + + //! Signal emitted when the channel list finishes loading or fails to load. + void channelListLoaded(); + + void noUpdateFound(); + +private slots: + void updateCheckFinished(bool notifyNoUpdate); + void updateCheckFailed(); + + void chanListDownloadFinished(bool notifyNoUpdate); + void chanListDownloadFailed(QString reason); + +private: + friend class UpdateCheckerTest; + + shared_qobject_ptr m_network; + + NetJob::Ptr indexJob; + QByteArray indexData; + NetJob::Ptr chanListJob; + QByteArray chanlistData; + + QString m_channelUrl; + + QList m_channels; + + /*! + * True while the system is checking for updates. + * If checkForUpdate is called while this is true, it will be ignored. + */ + bool m_updateChecking = false; + + /*! + * True if the channel list has loaded. + * If this is false, trying to check for updates will call updateChanList first. + */ + bool m_chanListLoaded = false; + + /*! + * Set to true while the channel list is currently loading. + */ + bool m_chanListLoading = false; + + /*! + * Set to true when checkForUpdate is called while the channel list isn't loaded. + * When the channel list finishes loading, if this is true, the update checker will check for updates. + */ + bool m_checkUpdateWaiting = false; + + int m_currentBuild = -1; + QString m_currentChannel; + QString m_currentRepoUrl; + + QString m_newRepoUrl; +}; + diff --git a/ultimmc/launcher/updater/UpdateChecker_test.cpp b/ultimmc/launcher/updater/UpdateChecker_test.cpp new file mode 100644 index 0000000..845ed99 --- /dev/null +++ b/ultimmc/launcher/updater/UpdateChecker_test.cpp @@ -0,0 +1,140 @@ +#include +#include + +#include "TestUtil.h" +#include "updater/UpdateChecker.h" + +Q_DECLARE_METATYPE(UpdateChecker::ChannelListEntry) + +bool operator==(const UpdateChecker::ChannelListEntry &e1, const UpdateChecker::ChannelListEntry &e2) +{ + qDebug() << e1.url << "vs" << e2.url; + return e1.id == e2.id && + e1.name == e2.name && + e1.description == e2.description && + e1.url == e2.url; +} + +QDebug operator<<(QDebug dbg, const UpdateChecker::ChannelListEntry &c) +{ + dbg.nospace() << "ChannelListEntry(id=" << c.id << " name=" << c.name << " description=" << c.description << " url=" << c.url << ")"; + return dbg.maybeSpace(); +} + +QString findTestDataUrl(const char *file) +{ + return QUrl::fromLocalFile(QFINDTESTDATA(file)).toString(); +} + +class UpdateCheckerTest : public QObject +{ + Q_OBJECT +private +slots: + void initTestCase() + { + + } + void cleanupTestCase() + { + + } + + void tst_ChannelListParsing_data() + { + QTest::addColumn("channelUrl"); + QTest::addColumn("hasChannels"); + QTest::addColumn("valid"); + QTest::addColumn >("result"); + + QTest::newRow("garbage") + << findTestDataUrl("data/garbageChannels.json") + << false + << false + << QList(); + QTest::newRow("errors") + << findTestDataUrl("data/errorChannels.json") + << false + << true + << QList(); + QTest::newRow("no channels") + << findTestDataUrl("data/noChannels.json") + << false + << true + << QList(); + QTest::newRow("one channel") + << findTestDataUrl("data/oneChannel.json") + << true + << true + << (QList() << UpdateChecker::ChannelListEntry{"develop", "Develop", "The channel called \"develop\"", "http://example.org/stuff"}); + QTest::newRow("several channels") + << findTestDataUrl("data/channels.json") + << true + << true + << (QList() + << UpdateChecker::ChannelListEntry{"develop", "Develop", "The channel called \"develop\"", findTestDataUrl("data")} + << UpdateChecker::ChannelListEntry{"stable", "Stable", "It's stable at least", findTestDataUrl("data")} + << UpdateChecker::ChannelListEntry{"42", "The Channel", "This is the channel that is going to answer all of your questions", "https://dent.me/tea"}); + } + void tst_ChannelListParsing() + { + QFETCH(QString, channelUrl); + QFETCH(bool, hasChannels); + QFETCH(bool, valid); + QFETCH(QList, result); + + shared_qobject_ptr nam = new QNetworkAccessManager(); + UpdateChecker checker(nam, channelUrl, 0); + + QSignalSpy channelListLoadedSpy(&checker, SIGNAL(channelListLoaded())); + QVERIFY(channelListLoadedSpy.isValid()); + + checker.updateChanList(false); + + if (valid) + { + QVERIFY(channelListLoadedSpy.wait()); + QCOMPARE(channelListLoadedSpy.size(), 1); + } + else + { + channelListLoadedSpy.wait(); + QCOMPARE(channelListLoadedSpy.size(), 0); + } + + QCOMPARE(checker.hasChannels(), hasChannels); + QCOMPARE(checker.getChannelList(), result); + } + + void tst_UpdateChecking() + { + QString channelUrl = findTestDataUrl("data/channels.json"); + int currentBuild = 2; + + shared_qobject_ptr nam = new QNetworkAccessManager(); + UpdateChecker checker(nam, channelUrl, currentBuild); + + QSignalSpy updateAvailableSpy(&checker, SIGNAL(updateAvailable(GoUpdate::Status))); + QVERIFY(updateAvailableSpy.isValid()); + QSignalSpy channelListLoadedSpy(&checker, SIGNAL(channelListLoaded())); + QVERIFY(channelListLoadedSpy.isValid()); + + checker.updateChanList(false); + QVERIFY(channelListLoadedSpy.wait()); + + qDebug() << "CWD:" << QDir::current().absolutePath(); + checker.m_channels[0].url = findTestDataUrl("data/"); + checker.checkForUpdate(false); + + QVERIFY(updateAvailableSpy.wait()); + + auto status = updateAvailableSpy.first().first().value(); + QCOMPARE(checker.m_channels[0].url, status.newRepoUrl); + QCOMPARE(3, status.newVersionId); + QCOMPARE(currentBuild, status.currentVersionId); + } +}; + +QTEST_GUILESS_MAIN(UpdateCheckerTest) + +#include "UpdateChecker_test.moc" diff --git a/ultimmc/launcher/updater/testdata/1.json b/ultimmc/launcher/updater/testdata/1.json new file mode 100644 index 0000000..7af7e52 --- /dev/null +++ b/ultimmc/launcher/updater/testdata/1.json @@ -0,0 +1,43 @@ +{ + "ApiVersion": 0, + "Id": 1, + "Name": "1.0.1", + "Files": [ + { + "Path": "fileOne", + "Sources": [ + { + "SourceType": "http", + "Url": "@TEST_DATA_URL@/fileOneA" + } + ], + "Executable": true, + "Perms": 493, + "MD5": "9eb84090956c484e32cb6c08455a667b" + }, + { + "Path": "fileTwo", + "Sources": [ + { + "SourceType": "http", + "Url": "@TEST_DATA_URL@/fileTwo" + } + ], + "Executable": false, + "Perms": 644, + "MD5": "38f94f54fa3eb72b0ea836538c10b043" + }, + { + "Path": "fileThree", + "Sources": [ + { + "SourceType": "http", + "Url": "@TEST_DATA_URL@/fileThree" + } + ], + "Executable": false, + "Perms": "750", + "MD5": "f12df554b21e320be6471d7154130e70" + } + ] +} diff --git a/ultimmc/launcher/updater/testdata/2.json b/ultimmc/launcher/updater/testdata/2.json new file mode 100644 index 0000000..96d430d --- /dev/null +++ b/ultimmc/launcher/updater/testdata/2.json @@ -0,0 +1,31 @@ +{ + "ApiVersion": 0, + "Id": 1, + "Name": "1.0.1", + "Files": [ + { + "Path": "fileOne", + "Sources": [ + { + "SourceType": "http", + "Url": "@TEST_DATA_URL@/fileOneB" + } + ], + "Executable": true, + "Perms": 493, + "MD5": "42915a71277c9016668cce7b82c6b577" + }, + { + "Path": "fileTwo", + "Sources": [ + { + "SourceType": "http", + "Url": "@TEST_DATA_URL@/fileTwo" + } + ], + "Executable": false, + "Perms": 644, + "MD5": "38f94f54fa3eb72b0ea836538c10b043" + } + ] +} diff --git a/ultimmc/launcher/updater/testdata/channels.json b/ultimmc/launcher/updater/testdata/channels.json new file mode 100644 index 0000000..5c6e42c --- /dev/null +++ b/ultimmc/launcher/updater/testdata/channels.json @@ -0,0 +1,23 @@ +{ + "format_version": 0, + "channels": [ + { + "id": "develop", + "name": "Develop", + "description": "The channel called \"develop\"", + "url": "@TEST_DATA_URL@" + }, + { + "id": "stable", + "name": "Stable", + "description": "It's stable at least", + "url": "@TEST_DATA_URL@" + }, + { + "id": "42", + "name": "The Channel", + "description": "This is the channel that is going to answer all of your questions", + "url": "https://dent.me/tea" + } + ] +} diff --git a/ultimmc/launcher/updater/testdata/errorChannels.json b/ultimmc/launcher/updater/testdata/errorChannels.json new file mode 100644 index 0000000..a2cb216 --- /dev/null +++ b/ultimmc/launcher/updater/testdata/errorChannels.json @@ -0,0 +1,23 @@ +{ + "format_version": 0, + "channels": [ + { + "id": "", + "name": "Develop", + "description": "The channel called \"develop\"", + "url": "http://example.org/stuff" + }, + { + "id": "stable", + "name": "", + "description": "It's stable at least", + "url": "ftp://username@host/path/to/stuff" + }, + { + "id": "42", + "name": "The Channel", + "description": "This is the channel that is going to answer all of your questions", + "url": "" + } + ] +} diff --git a/ultimmc/launcher/updater/testdata/fileOneA b/ultimmc/launcher/updater/testdata/fileOneA new file mode 100644 index 0000000..f2e4113 --- /dev/null +++ b/ultimmc/launcher/updater/testdata/fileOneA @@ -0,0 +1 @@ +stuff diff --git a/ultimmc/launcher/updater/testdata/fileOneB b/ultimmc/launcher/updater/testdata/fileOneB new file mode 100644 index 0000000..f9aba92 --- /dev/null +++ b/ultimmc/launcher/updater/testdata/fileOneB @@ -0,0 +1,3 @@ +stuff + +more stuff that came in the new version diff --git a/ultimmc/launcher/updater/testdata/fileThree b/ultimmc/launcher/updater/testdata/fileThree new file mode 100644 index 0000000..6353ff1 --- /dev/null +++ b/ultimmc/launcher/updater/testdata/fileThree @@ -0,0 +1 @@ +this is yet another file diff --git a/ultimmc/launcher/updater/testdata/fileTwo b/ultimmc/launcher/updater/testdata/fileTwo new file mode 100644 index 0000000..aad9a93 --- /dev/null +++ b/ultimmc/launcher/updater/testdata/fileTwo @@ -0,0 +1 @@ +some other stuff diff --git a/ultimmc/launcher/updater/testdata/garbageChannels.json b/ultimmc/launcher/updater/testdata/garbageChannels.json new file mode 100644 index 0000000..3443745 --- /dev/null +++ b/ultimmc/launcher/updater/testdata/garbageChannels.json @@ -0,0 +1,22 @@ +{ + "format_version": 0, + "channels": [ + { + "id": "develop", + "name": "Develop", + "description": "The channel called \"develop\"", +aa "url": "http://example.org/stuff" + }, +a "id": "stable", + "name": "Stable", + "description": "It's stable at least", + "url": "ftp://username@host/path/to/stuff" + }, + { + "id": "42"f + "name": "The Channel", + "description": "This is the channel that is going to answer all of your questions", + "url": "https://dent.me/tea" + } + ] +} diff --git a/ultimmc/launcher/updater/testdata/index.json b/ultimmc/launcher/updater/testdata/index.json new file mode 100644 index 0000000..867bdcf --- /dev/null +++ b/ultimmc/launcher/updater/testdata/index.json @@ -0,0 +1,9 @@ +{ + "ApiVersion": 0, + "Versions": [ + { "Id": 0, "Name": "1.0.0" }, + { "Id": 1, "Name": "1.0.1" }, + { "Id": 2, "Name": "1.0.2" }, + { "Id": 3, "Name": "1.0.3" } + ] +} diff --git a/ultimmc/launcher/updater/testdata/noChannels.json b/ultimmc/launcher/updater/testdata/noChannels.json new file mode 100644 index 0000000..7698898 --- /dev/null +++ b/ultimmc/launcher/updater/testdata/noChannels.json @@ -0,0 +1,5 @@ +{ + "format_version": 0, + "channels": [ + ] +} diff --git a/ultimmc/launcher/updater/testdata/oneChannel.json b/ultimmc/launcher/updater/testdata/oneChannel.json new file mode 100644 index 0000000..cc8ed25 --- /dev/null +++ b/ultimmc/launcher/updater/testdata/oneChannel.json @@ -0,0 +1,11 @@ +{ + "format_version": 0, + "channels": [ + { + "id": "develop", + "name": "Develop", + "description": "The channel called \"develop\"", + "url": "http://example.org/stuff" + } + ] +} diff --git a/ultimmc/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml b/ultimmc/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml new file mode 100644 index 0000000..09c162c --- /dev/null +++ b/ultimmc/launcher/updater/testdata/tst_DownloadTask-test_writeInstallScript.xml @@ -0,0 +1,17 @@ + + + + sourceOne + destOne + 0777 + + + MultiMC.exe + M/u/l/t/i/M/C/e/x/e + 0644 + + + + toDelete.abc + + diff --git a/ultimmc/libraries/LocalPeer/CMakeLists.txt b/ultimmc/libraries/LocalPeer/CMakeLists.txt new file mode 100644 index 0000000..1e7557e --- /dev/null +++ b/ultimmc/libraries/LocalPeer/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.1) +project(LocalPeer) + +find_package(Qt5 COMPONENTS Core Network REQUIRED) + +set(SINGLE_SOURCES +src/LocalPeer.cpp +src/LockedFile.cpp +src/LockedFile.h +include/LocalPeer.h +) + +if(UNIX) + list(APPEND SINGLE_SOURCES + src/LockedFile_unix.cpp + ) +endif() + +if(WIN32) + list(APPEND SINGLE_SOURCES + src/LockedFile_win.cpp + ) +endif() + +add_library(LocalPeer STATIC ${SINGLE_SOURCES}) +target_include_directories(LocalPeer PUBLIC include) + +target_link_libraries(LocalPeer Qt5::Core Qt5::Network) diff --git a/ultimmc/libraries/LocalPeer/include/LocalPeer.h b/ultimmc/libraries/LocalPeer/include/LocalPeer.h new file mode 100644 index 0000000..3619ed5 --- /dev/null +++ b/ultimmc/libraries/LocalPeer/include/LocalPeer.h @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Solutions component. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#pragma once +#include +#include +#include + + +class QLocalServer; +class LockedFile; + +class ApplicationId +{ +public: /* methods */ + // traditional app = installed system wide and used in a multi-user environment + static ApplicationId fromTraditionalApp(); + // ID based on a path with all the application data (no two instances with the same data path should run) + static ApplicationId fromPathAndVersion(const QString & dataPath, const QString & version); + // custom ID + static ApplicationId fromCustomId(const QString & id); + // custom ID, based on a raw string previously acquired from 'toString' + static ApplicationId fromRawString(const QString & id); + + + QString toString() + { + return m_id; + } + +private: /* methods */ + ApplicationId(const QString & value) + { + m_id = value; + } + +private: /* data */ + QString m_id; +}; + +class LocalPeer : public QObject +{ + Q_OBJECT + +public: + LocalPeer(QObject *parent, const ApplicationId &appId); + ~LocalPeer(); + bool isClient(); + bool sendMessage(const QByteArray &message, int timeout); + ApplicationId applicationId() const; + +Q_SIGNALS: + void messageReceived(const QByteArray &message); + +protected Q_SLOTS: + void receiveConnection(); + +protected: + ApplicationId id; + QString socketName; + std::unique_ptr server; + std::unique_ptr lockFile; +}; diff --git a/ultimmc/libraries/LocalPeer/src/LocalPeer.cpp b/ultimmc/libraries/LocalPeer/src/LocalPeer.cpp new file mode 100644 index 0000000..cb21846 --- /dev/null +++ b/ultimmc/libraries/LocalPeer/src/LocalPeer.cpp @@ -0,0 +1,240 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Solutions component. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#include "LocalPeer.h" +#include +#include +#include +#include +#include +#include +#include "LockedFile.h" + +#if defined(Q_OS_WIN) +#include +#include +typedef BOOL(WINAPI*PProcessIdToSessionId)(DWORD,DWORD*); +static PProcessIdToSessionId pProcessIdToSessionId = 0; +#endif +#if defined(Q_OS_UNIX) +#include +#include +#endif + +#include +#include +#include + +static const char* ack = "ack"; + +ApplicationId ApplicationId::fromTraditionalApp() +{ + QString protoId = QCoreApplication::applicationFilePath(); +#if defined(Q_OS_WIN) + protoId = protoId.toLower(); +#endif + auto prefix = protoId.section(QLatin1Char('/'), -1); + prefix.remove(QRegExp("[^a-zA-Z]")); + prefix.truncate(6); + QByteArray idc = protoId.toUtf8(); + quint16 idNum = qChecksum(idc.constData(), idc.size()); + auto socketName = QLatin1String("qtsingleapp-") + prefix + QLatin1Char('-') + QString::number(idNum, 16); +#if defined(Q_OS_WIN) + if (!pProcessIdToSessionId) + { + QLibrary lib("kernel32"); + pProcessIdToSessionId = (PProcessIdToSessionId)lib.resolve("ProcessIdToSessionId"); + } + if (pProcessIdToSessionId) + { + DWORD sessionId = 0; + pProcessIdToSessionId(GetCurrentProcessId(), &sessionId); + socketName += QLatin1Char('-') + QString::number(sessionId, 16); + } +#else + socketName += QLatin1Char('-') + QString::number(::getuid(), 16); +#endif + return ApplicationId(socketName); +} + +ApplicationId ApplicationId::fromPathAndVersion(const QString& dataPath, const QString& version) +{ + QCryptographicHash shasum(QCryptographicHash::Algorithm::Sha1); + QString result = dataPath + QLatin1Char('-') + version; + shasum.addData(result.toUtf8()); + return ApplicationId(QLatin1String("qtsingleapp-") + QString::fromLatin1(shasum.result().toHex())); +} + +ApplicationId ApplicationId::fromCustomId(const QString& id) +{ + return ApplicationId(QLatin1String("qtsingleapp-") + id); +} + +ApplicationId ApplicationId::fromRawString(const QString& id) +{ + return ApplicationId(id); +} + +LocalPeer::LocalPeer(QObject * parent, const ApplicationId &appId) + : QObject(parent), id(appId) +{ + socketName = id.toString(); + server.reset(new QLocalServer()); + QString lockName = QDir(QDir::tempPath()).absolutePath() + QLatin1Char('/') + socketName + QLatin1String("-lockfile"); + lockFile.reset(new LockedFile(lockName)); + lockFile->open(QIODevice::ReadWrite); +} + +LocalPeer::~LocalPeer() +{ +} + +ApplicationId LocalPeer::applicationId() const +{ + return id; +} + +bool LocalPeer::isClient() +{ + if (lockFile->isLocked()) + return false; + + if (!lockFile->lock(LockedFile::WriteLock, false)) + return true; + + bool res = server->listen(socketName); +#if defined(Q_OS_UNIX) + // ### Workaround + if (!res && server->serverError() == QAbstractSocket::AddressInUseError) { + QFile::remove(QDir::cleanPath(QDir::tempPath())+QLatin1Char('/')+socketName); + res = server->listen(socketName); + } +#endif + if (!res) + qWarning("QtSingleCoreApplication: listen on local socket failed, %s", qPrintable(server->errorString())); + QObject::connect(server.get(), SIGNAL(newConnection()), SLOT(receiveConnection())); + return false; +} + + +bool LocalPeer::sendMessage(const QByteArray &message, int timeout) +{ + if (!isClient()) + return false; + + QLocalSocket socket; + bool connOk = false; + for(int i = 0; i < 2; i++) { + // Try twice, in case the other instance is just starting up + socket.connectToServer(socketName); + connOk = socket.waitForConnected(timeout/2); + if (connOk || i) + { + break; + } + std::this_thread::sleep_for(std::chrono::milliseconds(250)); + } + if (!connOk) + { + return false; + } + + QByteArray uMsg(message); + QDataStream ds(&socket); + + ds.writeBytes(uMsg.constData(), uMsg.size()); + if(!socket.waitForBytesWritten(timeout)) + { + return false; + } + + // wait for 'ack' + if(!socket.waitForReadyRead(timeout)) + { + return false; + } + + // make sure we got 'ack' + if(!(socket.read(qstrlen(ack)) == ack)) + { + return false; + } + return true; +} + + +void LocalPeer::receiveConnection() +{ + QLocalSocket* socket = server->nextPendingConnection(); + if (!socket) + { + return; + } + + while (socket->bytesAvailable() < (int)sizeof(quint32)) + { + socket->waitForReadyRead(); + } + QDataStream ds(socket); + QByteArray uMsg; + quint32 remaining; + ds >> remaining; + uMsg.resize(remaining); + int got = 0; + char* uMsgBuf = uMsg.data(); + do + { + got = ds.readRawData(uMsgBuf, remaining); + remaining -= got; + uMsgBuf += got; + } while (remaining && got >= 0 && socket->waitForReadyRead(2000)); + if (got < 0) + { + qWarning("QtLocalPeer: Message reception failed %s", socket->errorString().toLatin1().constData()); + delete socket; + return; + } + socket->write(ack, qstrlen(ack)); + socket->waitForBytesWritten(1000); + socket->waitForDisconnected(1000); // make sure client reads ack + delete socket; + emit messageReceived(uMsg); //### (might take a long time to return) +} diff --git a/ultimmc/libraries/LocalPeer/src/LockedFile.cpp b/ultimmc/libraries/LocalPeer/src/LockedFile.cpp new file mode 100644 index 0000000..73294a1 --- /dev/null +++ b/ultimmc/libraries/LocalPeer/src/LockedFile.cpp @@ -0,0 +1,193 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Solutions component. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "LockedFile.h" + +/*! + \class QtLockedFile + + \brief The QtLockedFile class extends QFile with advisory locking + functions. + + A file may be locked in read or write mode. Multiple instances of + \e QtLockedFile, created in multiple processes running on the same + machine, may have a file locked in read mode. Exactly one instance + may have it locked in write mode. A read and a write lock cannot + exist simultaneously on the same file. + + The file locks are advisory. This means that nothing prevents + another process from manipulating a locked file using QFile or + file system functions offered by the OS. Serialization is only + guaranteed if all processes that access the file use + QLockedFile. Also, while holding a lock on a file, a process + must not open the same file again (through any API), or locks + can be unexpectedly lost. + + The lock provided by an instance of \e QtLockedFile is released + whenever the program terminates. This is true even when the + program crashes and no destructors are called. +*/ + +/*! \enum QtLockedFile::LockMode + + This enum describes the available lock modes. + + \value ReadLock A read lock. + \value WriteLock A write lock. + \value NoLock Neither a read lock nor a write lock. +*/ + +/*! + Constructs an unlocked \e QtLockedFile object. This constructor + behaves in the same way as \e QFile::QFile(). + + \sa QFile::QFile() +*/ +LockedFile::LockedFile() + : QFile() +{ +#ifdef Q_OS_WIN + wmutex = 0; + rmutex = 0; +#endif + m_lock_mode = NoLock; +} + +/*! + Constructs an unlocked QtLockedFile object with file \a name. This + constructor behaves in the same way as \e QFile::QFile(const + QString&). + + \sa QFile::QFile() +*/ +LockedFile::LockedFile(const QString &name) + : QFile(name) +{ +#ifdef Q_OS_WIN + wmutex = 0; + rmutex = 0; +#endif + m_lock_mode = NoLock; +} + +/*! +Opens the file in OpenMode \a mode. + +This is identical to QFile::open(), with the one exception that the +Truncate mode flag is disallowed. Truncation would conflict with the +advisory file locking, since the file would be modified before the +write lock is obtained. If truncation is required, use resize(0) +after obtaining the write lock. + +Returns true if successful; otherwise false. + +\sa QFile::open(), QFile::resize() +*/ +bool LockedFile::open(OpenMode mode) +{ + if (mode & QIODevice::Truncate) { + qWarning("QtLockedFile::open(): Truncate mode not allowed."); + return false; + } + return QFile::open(mode); +} + +/*! + Returns \e true if this object has a in read or write lock; + otherwise returns \e false. + + \sa lockMode() +*/ +bool LockedFile::isLocked() const +{ + return m_lock_mode != NoLock; +} + +/*! + Returns the type of lock currently held by this object, or \e + QtLockedFile::NoLock. + + \sa isLocked() +*/ +LockedFile::LockMode LockedFile::lockMode() const +{ + return m_lock_mode; +} + +/*! + \fn bool QtLockedFile::lock(LockMode mode, bool block = true) + + Obtains a lock of type \a mode. The file must be opened before it + can be locked. + + If \a block is true, this function will block until the lock is + aquired. If \a block is false, this function returns \e false + immediately if the lock cannot be aquired. + + If this object already has a lock of type \a mode, this function + returns \e true immediately. If this object has a lock of a + different type than \a mode, the lock is first released and then a + new lock is obtained. + + This function returns \e true if, after it executes, the file is + locked by this object, and \e false otherwise. + + \sa unlock(), isLocked(), lockMode() +*/ + +/*! + \fn bool QtLockedFile::unlock() + + Releases a lock. + + If the object has no lock, this function returns immediately. + + This function returns \e true if, after it executes, the file is + not locked by this object, and \e false otherwise. + + \sa lock(), isLocked(), lockMode() +*/ + +/*! + \fn QtLockedFile::~QtLockedFile() + + Destroys the \e QtLockedFile object. If any locks were held, they + are released. +*/ diff --git a/ultimmc/libraries/LocalPeer/src/LockedFile.h b/ultimmc/libraries/LocalPeer/src/LockedFile.h new file mode 100644 index 0000000..2f29ee2 --- /dev/null +++ b/ultimmc/libraries/LocalPeer/src/LockedFile.h @@ -0,0 +1,77 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Solutions component. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#pragma once + +#include +#ifdef Q_OS_WIN +#include +#endif + +class LockedFile : public QFile +{ +public: + enum LockMode { NoLock = 0, ReadLock, WriteLock }; + + LockedFile(); + LockedFile(const QString &name); + ~LockedFile(); + + bool open(OpenMode mode); + + bool lock(LockMode mode, bool block = true); + bool unlock(); + bool isLocked() const; + LockMode lockMode() const; + + private: + +#ifdef Q_OS_WIN + Qt::HANDLE wmutex; + Qt::HANDLE rmutex; + QVector rmutexes; + QString mutexname; + + Qt::HANDLE getMutexHandle(int idx, bool doCreate); + bool waitMutex(Qt::HANDLE mutex, bool doBlock); +#endif + + LockMode m_lock_mode; +}; diff --git a/ultimmc/libraries/LocalPeer/src/LockedFile_unix.cpp b/ultimmc/libraries/LocalPeer/src/LockedFile_unix.cpp new file mode 100644 index 0000000..6becc89 --- /dev/null +++ b/ultimmc/libraries/LocalPeer/src/LockedFile_unix.cpp @@ -0,0 +1,114 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Solutions component. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include +#include + +#include "LockedFile.h" + +bool LockedFile::lock(LockMode mode, bool block) +{ + if (!isOpen()) { + qWarning("QtLockedFile::lock(): file is not opened"); + return false; + } + + if (mode == NoLock) + return unlock(); + + if (mode == m_lock_mode) + return true; + + if (m_lock_mode != NoLock) + unlock(); + + struct flock fl; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 0; + fl.l_type = (mode == ReadLock) ? F_RDLCK : F_WRLCK; + int cmd = block ? F_SETLKW : F_SETLK; + int ret = fcntl(handle(), cmd, &fl); + + if (ret == -1) { + if (errno != EINTR && errno != EAGAIN) + qWarning("QtLockedFile::lock(): fcntl: %s", strerror(errno)); + return false; + } + + + m_lock_mode = mode; + return true; +} + + +bool LockedFile::unlock() +{ + if (!isOpen()) { + qWarning("QtLockedFile::unlock(): file is not opened"); + return false; + } + + if (!isLocked()) + return true; + + struct flock fl; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 0; + fl.l_type = F_UNLCK; + int ret = fcntl(handle(), F_SETLKW, &fl); + + if (ret == -1) { + qWarning("QtLockedFile::lock(): fcntl: %s", strerror(errno)); + return false; + } + + m_lock_mode = NoLock; + return true; +} + +LockedFile::~LockedFile() +{ + if (isOpen()) + unlock(); +} diff --git a/ultimmc/libraries/LocalPeer/src/LockedFile_win.cpp b/ultimmc/libraries/LocalPeer/src/LockedFile_win.cpp new file mode 100644 index 0000000..93d2c73 --- /dev/null +++ b/ultimmc/libraries/LocalPeer/src/LockedFile_win.cpp @@ -0,0 +1,205 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the Qt Solutions component. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of Digia Plc and its Subsidiary(-ies) nor the names +** of its contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "LockedFile.h" +#include +#include + +#define MUTEX_PREFIX "QtLockedFile mutex " +// Maximum number of concurrent read locks. Must not be greater than MAXIMUM_WAIT_OBJECTS +#define MAX_READERS MAXIMUM_WAIT_OBJECTS + +Qt::HANDLE LockedFile::getMutexHandle(int idx, bool doCreate) +{ + if (mutexname.isEmpty()) { + QFileInfo fi(*this); + mutexname = QString::fromLatin1(MUTEX_PREFIX) + + fi.absoluteFilePath().toLower(); + } + QString mname(mutexname); + if (idx >= 0) + mname += QString::number(idx); + + Qt::HANDLE mutex; + if (doCreate) { + mutex = CreateMutexW(NULL, FALSE, (LPCWSTR)mname.utf16()); + if (!mutex) { + qErrnoWarning("QtLockedFile::lock(): CreateMutex failed"); + return 0; + } + } + else { + mutex = OpenMutexW(SYNCHRONIZE | MUTEX_MODIFY_STATE, FALSE, (LPCWSTR)mname.utf16()); + if (!mutex) { + if (GetLastError() != ERROR_FILE_NOT_FOUND) + qErrnoWarning("QtLockedFile::lock(): OpenMutex failed"); + return 0; + } + } + return mutex; +} + +bool LockedFile::waitMutex(Qt::HANDLE mutex, bool doBlock) +{ + Q_ASSERT(mutex); + DWORD res = WaitForSingleObject(mutex, doBlock ? INFINITE : 0); + switch (res) { + case WAIT_OBJECT_0: + case WAIT_ABANDONED: + return true; + break; + case WAIT_TIMEOUT: + break; + default: + qErrnoWarning("QtLockedFile::lock(): WaitForSingleObject failed"); + } + return false; +} + + + +bool LockedFile::lock(LockMode mode, bool block) +{ + if (!isOpen()) { + qWarning("QtLockedFile::lock(): file is not opened"); + return false; + } + + if (mode == NoLock) + return unlock(); + + if (mode == m_lock_mode) + return true; + + if (m_lock_mode != NoLock) + unlock(); + + if (!wmutex && !(wmutex = getMutexHandle(-1, true))) + return false; + + if (!waitMutex(wmutex, block)) + return false; + + if (mode == ReadLock) { + int idx = 0; + for (; idx < MAX_READERS; idx++) { + rmutex = getMutexHandle(idx, false); + if (!rmutex || waitMutex(rmutex, false)) + break; + CloseHandle(rmutex); + } + bool ok = true; + if (idx >= MAX_READERS) { + qWarning("QtLockedFile::lock(): too many readers"); + rmutex = 0; + ok = false; + } + else if (!rmutex) { + rmutex = getMutexHandle(idx, true); + if (!rmutex || !waitMutex(rmutex, false)) + ok = false; + } + if (!ok && rmutex) { + CloseHandle(rmutex); + rmutex = 0; + } + ReleaseMutex(wmutex); + if (!ok) + return false; + } + else { + Q_ASSERT(rmutexes.isEmpty()); + for (int i = 0; i < MAX_READERS; i++) { + Qt::HANDLE mutex = getMutexHandle(i, false); + if (mutex) + rmutexes.append(mutex); + } + if (rmutexes.size()) { + DWORD res = WaitForMultipleObjects(rmutexes.size(), rmutexes.constData(), + TRUE, block ? INFINITE : 0); + if (res != WAIT_OBJECT_0 && res != WAIT_ABANDONED) { + if (res != WAIT_TIMEOUT) + qErrnoWarning("QtLockedFile::lock(): WaitForMultipleObjects failed"); + m_lock_mode = WriteLock; // trick unlock() to clean up - semiyucky + unlock(); + return false; + } + } + } + + m_lock_mode = mode; + return true; +} + +bool LockedFile::unlock() +{ + if (!isOpen()) { + qWarning("QtLockedFile::unlock(): file is not opened"); + return false; + } + + if (!isLocked()) + return true; + + if (m_lock_mode == ReadLock) { + ReleaseMutex(rmutex); + CloseHandle(rmutex); + rmutex = 0; + } + else { + foreach(Qt::HANDLE mutex, rmutexes) { + ReleaseMutex(mutex); + CloseHandle(mutex); + } + rmutexes.clear(); + ReleaseMutex(wmutex); + } + + m_lock_mode = LockedFile::NoLock; + return true; +} + +LockedFile::~LockedFile() +{ + if (isOpen()) + unlock(); + if (wmutex) + CloseHandle(wmutex); +} diff --git a/ultimmc/libraries/README.md b/ultimmc/libraries/README.md new file mode 100644 index 0000000..3950588 --- /dev/null +++ b/ultimmc/libraries/README.md @@ -0,0 +1,188 @@ +# Third-party libraries + +This folder has third-party or otherwise external libraries needed for other parts to work. + +## classparser +A simplistic parser for Java class files. + +This library has served as a base for some (much more full-featured and advanced) work under NDA for AVG. It, however, should NOT be confused with that work. + +Copyright belongs to Petr Mrázek, unless explicitly stated otherwise in the source files. Available under the Apache 2.0 license. + +## ganalytics +A Google Analytics library for Qt. + +BSD licensed, derived from [qt-google-analytics](https://github.com/HSAnet/qt-google-analytics). + +Modifications include better handling of IP anonymization (can be enabled) and general improvements of the API (application handles persistence and ID generation instead of the library). + +## hoedown +Hoedown is a revived fork of Sundown, the Markdown parser based on the original code of the Upskirt library by Natacha Porté. + +See [github repo](https://github.com/hoedown/hoedown). + +## iconfix +This was originally part of the razor-qt project and the Qt toolkit, respecitvely. Its sole purpose is to reimplement Qt's icon loading logic to prevent it from using any platform plugins that could break icon loading. + +Licensed under LGPL 2.1 + +## javacheck +Simple Java tool that prints the JVM details - version and platform bitness. + +Do what you want with it. It is so trivial that noone cares. + +## Katabasis +Oauth2 library customized for Microsoft authentication. + +This is a fork of the [O2 library](https://github.com/pipacs/o2). + +MIT licensed. + +## launcher +Java launcher part for Minecraft. + +It: +* Starts a process +* Waits for a launch script on stdin +* Consumes the launch script you feed it +* Proceeds with launch when it gets the `launcher` command + +This means the process is essentially idle until the final command is sent. You can, for example, attach a profiler before you send it. + +A `legacy` and `onesix` launchers are available. + +* `legacy` is intended for use with Minecraft versions < 1.6 and is deprecated. +* `onesix` can handle launching any Minecraft version, at the cost of some extra features `legacy` enables (custom window icon and title). + +Example (some parts have been censored): +``` +mod legacyjavafixer-1.0 +mainClass net.minecraft.launchwrapper.Launch +param --username +param CENSORED +param --version +param MultiMC5 +param --gameDir +param /home/peterix/minecraft/FTB/17ForgeTest/minecraft +param --assetsDir +param /home/peterix/minecraft/mmc5/assets +param --assetIndex +param 1.7.10 +param --uuid +param CENSORED +param --accessToken +param CENSORED +param --userProperties +param {} +param --userType +param mojang +param --tweakClass +param cpw.mods.fml.common.launcher.FMLTweaker +windowTitle MultiMC: 172ForgeTest +windowParams 854x480 +userName CENSORED +sessionId token:CENSORED:CENSORED +cp /home/peterix/minecraft/FTB/libraries/com/mojang/realms/1.3.5/realms-1.3.5.jar +cp /home/peterix/minecraft/FTB/libraries/org/apache/commons/commons-compress/1.8.1/commons-compress-1.8.1.jar +cp /home/peterix/minecraft/FTB/libraries/org/apache/httpcomponents/httpclient/4.3.3/httpclient-4.3.3.jar +cp /home/peterix/minecraft/FTB/libraries/commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar +cp /home/peterix/minecraft/FTB/libraries/org/apache/httpcomponents/httpcore/4.3.2/httpcore-4.3.2.jar +cp /home/peterix/minecraft/FTB/libraries/java3d/vecmath/1.3.1/vecmath-1.3.1.jar +cp /home/peterix/minecraft/FTB/libraries/net/sf/trove4j/trove4j/3.0.3/trove4j-3.0.3.jar +cp /home/peterix/minecraft/FTB/libraries/com/ibm/icu/icu4j-core-mojang/51.2/icu4j-core-mojang-51.2.jar +cp /home/peterix/minecraft/FTB/libraries/net/sf/jopt-simple/jopt-simple/4.5/jopt-simple-4.5.jar +cp /home/peterix/minecraft/FTB/libraries/com/paulscode/codecjorbis/20101023/codecjorbis-20101023.jar +cp /home/peterix/minecraft/FTB/libraries/com/paulscode/codecwav/20101023/codecwav-20101023.jar +cp /home/peterix/minecraft/FTB/libraries/com/paulscode/libraryjavasound/20101123/libraryjavasound-20101123.jar +cp /home/peterix/minecraft/FTB/libraries/com/paulscode/librarylwjglopenal/20100824/librarylwjglopenal-20100824.jar +cp /home/peterix/minecraft/FTB/libraries/com/paulscode/soundsystem/20120107/soundsystem-20120107.jar +cp /home/peterix/minecraft/FTB/libraries/io/netty/netty-all/4.0.10.Final/netty-all-4.0.10.Final.jar +cp /home/peterix/minecraft/FTB/libraries/com/google/guava/guava/16.0/guava-16.0.jar +cp /home/peterix/minecraft/FTB/libraries/org/apache/commons/commons-lang3/3.2.1/commons-lang3-3.2.1.jar +cp /home/peterix/minecraft/FTB/libraries/commons-io/commons-io/2.4/commons-io-2.4.jar +cp /home/peterix/minecraft/FTB/libraries/commons-codec/commons-codec/1.9/commons-codec-1.9.jar +cp /home/peterix/minecraft/FTB/libraries/net/java/jinput/jinput/2.0.5/jinput-2.0.5.jar +cp /home/peterix/minecraft/FTB/libraries/net/java/jutils/jutils/1.0.0/jutils-1.0.0.jar +cp /home/peterix/minecraft/FTB/libraries/com/google/code/gson/gson/2.2.4/gson-2.2.4.jar +cp /home/peterix/minecraft/FTB/libraries/com/mojang/authlib/1.5.16/authlib-1.5.16.jar +cp /home/peterix/minecraft/FTB/libraries/org/apache/logging/log4j/log4j-api/2.0-beta9/log4j-api-2.0-beta9.jar +cp /home/peterix/minecraft/FTB/libraries/org/apache/logging/log4j/log4j-core/2.0-beta9/log4j-core-2.0-beta9.jar +cp /home/peterix/minecraft/FTB/libraries/org/lwjgl/lwjgl/lwjgl/2.9.1/lwjgl-2.9.1.jar +cp /home/peterix/minecraft/FTB/libraries/org/lwjgl/lwjgl/lwjgl_util/2.9.1/lwjgl_util-2.9.1.jar +cp /home/peterix/minecraft/FTB/libraries/tv/twitch/twitch/5.16/twitch-5.16.jar +cp /home/peterix/minecraft/FTB/libraries/net/minecraftforge/forge/1.7.10-10.13.0.1178/forge-1.7.10-10.13.0.1178.jar +cp /home/peterix/minecraft/FTB/libraries/net/minecraft/launchwrapper/1.9/launchwrapper-1.9.jar +cp /home/peterix/minecraft/FTB/libraries/org/ow2/asm/asm-all/4.1/asm-all-4.1.jar +cp /home/peterix/minecraft/FTB/libraries/com/typesafe/akka/akka-actor_2.11/2.3.3/akka-actor_2.11-2.3.3.jar +cp /home/peterix/minecraft/FTB/libraries/com/typesafe/config/1.2.1/config-1.2.1.jar +cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/scala-actors-migration_2.11/1.1.0/scala-actors-migration_2.11-1.1.0.jar +cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/scala-compiler/2.11.1/scala-compiler-2.11.1.jar +cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/plugins/scala-continuations-library_2.11/1.0.2/scala-continuations-library_2.11-1.0.2.jar +cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/plugins/scala-continuations-plugin_2.11.1/1.0.2/scala-continuations-plugin_2.11.1-1.0.2.jar +cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/scala-library/2.11.1/scala-library-2.11.1.jar +cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/scala-parser-combinators_2.11/1.0.1/scala-parser-combinators_2.11-1.0.1.jar +cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/scala-reflect/2.11.1/scala-reflect-2.11.1.jar +cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/scala-swing_2.11/1.0.1/scala-swing_2.11-1.0.1.jar +cp /home/peterix/minecraft/FTB/libraries/org/scala-lang/scala-xml_2.11/1.0.2/scala-xml_2.11-1.0.2.jar +cp /home/peterix/minecraft/FTB/libraries/lzma/lzma/0.0.1/lzma-0.0.1.jar +ext /home/peterix/minecraft/FTB/libraries/org/lwjgl/lwjgl/lwjgl-platform/2.9.1/lwjgl-platform-2.9.1-natives-linux.jar +ext /home/peterix/minecraft/FTB/libraries/net/java/jinput/jinput-platform/2.0.5/jinput-platform-2.0.5-natives-linux.jar +natives /home/peterix/minecraft/FTB/17ForgeTest/natives +cp /home/peterix/minecraft/FTB/versions/1.7.10/1.7.10.jar +launcher onesix +``` + +Available under the Apache 2.0 license. + +## libnbtplusplus +libnbt++ is a free C++ library for Minecraft's file format Named Binary Tag (NBT). It can read and write compressed and uncompressed NBT files and provides a code interface for working with NBT data. + +See [github repo](https://github.com/ljfa-ag/libnbtplusplus). + +Available either under LGPL version 3 or later. + +## LocalPeer +Library for making only one instance of the application run at all times. + +BSD licensed, derived from [QtSingleApplication](https://github.com/qtproject/qt-solutions/tree/master/qtsingleapplication). + +Changes are made to make the code more generic and useful in less usual conditions. + +## optional-bare + +A simple single-file header-only version of a C++17-like optional for default-constructible, copyable types, for C++98 and later. + +Imported from: https://github.com/martinmoene/optional-bare/commit/0bb1d183bcee1e854c4ea196b533252c51f98b81 + +Boost Software License - Version 1.0 + +## quazip + +A zip manipulation library, forked for MultiMC's use. + +LGPL 2.1 + +## rainbow +Color functions extracted from [KGuiAddons](https://inqlude.org/libraries/kguiaddons.html). Used for adaptive text coloring. + +Available either under LGPL version 2.1 or later. + +## systeminfo + +A MultiMC-specific library for probing system information. + +Apache 2.0 + +## tomlc99 + +A TOML language parser. Used by Forge 1.14+ to store mod metadata. + +See [github repo](https://github.com/cktan/tomlc99). + +Licenced under the MIT licence. + +## xz-embedded + +Tiny implementation of LZMA2 de/compression. This format is only used by Forge to save bandwidth. + +Public domain. diff --git a/ultimmc/libraries/classparser/CMakeLists.txt b/ultimmc/libraries/classparser/CMakeLists.txt new file mode 100644 index 0000000..c07e871 --- /dev/null +++ b/ultimmc/libraries/classparser/CMakeLists.txt @@ -0,0 +1,41 @@ +project(classparser) + +set(CMAKE_AUTOMOC ON) + +######## Check endianness ######## +include(TestBigEndian) +test_big_endian(BIGENDIAN) +if(${BIGENDIAN}) + add_definitions(-DMULTIMC_BIG_ENDIAN) +endif(${BIGENDIAN}) + +# Find Qt +find_package(Qt5Core REQUIRED) + +# Include Qt headers. +include_directories(${Qt5Base_INCLUDE_DIRS}) + +set(CLASSPARSER_HEADERS +# Public headers +include/classparser_config.h +include/classparser.h + +# Private headers +src/annotations.h +src/classfile.h +src/constants.h +src/errors.h +src/javaendian.h +src/membuffer.h +) + +set(CLASSPARSER_SOURCES +src/classparser.cpp +src/annotations.cpp +) + +add_definitions(-DCLASSPARSER_LIBRARY) + +add_library(Launcher_classparser STATIC ${CLASSPARSER_SOURCES} ${CLASSPARSER_HEADERS}) +target_include_directories(Launcher_classparser PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(Launcher_classparser Launcher_quazip Qt5::Core) diff --git a/ultimmc/libraries/classparser/include/classparser.h b/ultimmc/libraries/classparser/include/classparser.h new file mode 100644 index 0000000..3660026 --- /dev/null +++ b/ultimmc/libraries/classparser/include/classparser.h @@ -0,0 +1,27 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Authors: Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once +#include +#include "classparser_config.h" + +namespace classparser +{ +/** + * @brief Get the version from a minecraft.jar by parsing its class files. Expensive! + */ +QString GetMinecraftJarVersion(QString jar); +} diff --git a/ultimmc/libraries/classparser/include/classparser_config.h b/ultimmc/libraries/classparser/include/classparser_config.h new file mode 100644 index 0000000..7bfae7c --- /dev/null +++ b/ultimmc/libraries/classparser/include/classparser_config.h @@ -0,0 +1,22 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#ifdef CLASSPARSER_LIBRARY +#define CLASSPARSER_EXPORT Q_DECL_EXPORT +#else +#define CLASSPARSER_EXPORT Q_DECL_IMPORT +#endif diff --git a/ultimmc/libraries/classparser/src/annotations.cpp b/ultimmc/libraries/classparser/src/annotations.cpp new file mode 100644 index 0000000..18a9e88 --- /dev/null +++ b/ultimmc/libraries/classparser/src/annotations.cpp @@ -0,0 +1,85 @@ +#include "classfile.h" +#include "annotations.h" +#include + +namespace java +{ +std::string annotation::toString() +{ + std::ostringstream ss; + ss << "Annotation type : " << type_index << " - " << pool[type_index].str_data << std::endl; + ss << "Contains " << name_val_pairs.size() << " pairs:" << std::endl; + for (unsigned i = 0; i < name_val_pairs.size(); i++) + { + std::pair &val = name_val_pairs[i]; + auto name_idx = val.first; + ss << pool[name_idx].str_data << "(" << name_idx << ")" + << " = " << val.second->toString() << std::endl; + } + return ss.str(); +} + +annotation *annotation::read(util::membuffer &input, constant_pool &pool) +{ + uint16_t type_index = 0; + input.read_be(type_index); + annotation *ann = new annotation(type_index, pool); + + uint16_t num_pairs = 0; + input.read_be(num_pairs); + while (num_pairs) + { + uint16_t name_idx = 0; + // read name index + input.read_be(name_idx); + auto elem = element_value::readElementValue(input, pool); + // read value + ann->add_pair(name_idx, elem); + num_pairs--; + } + return ann; +} + +element_value *element_value::readElementValue(util::membuffer &input, + java::constant_pool &pool) +{ + element_value_type type = INVALID; + input.read(type); + uint16_t index = 0; + uint16_t index2 = 0; + std::vector vals; + switch (type) + { + case PRIMITIVE_BYTE: + case PRIMITIVE_CHAR: + case PRIMITIVE_DOUBLE: + case PRIMITIVE_FLOAT: + case PRIMITIVE_INT: + case PRIMITIVE_LONG: + case PRIMITIVE_SHORT: + case PRIMITIVE_BOOLEAN: + case STRING: + input.read_be(index); + return new element_value_simple(type, index, pool); + case ENUM_CONSTANT: + input.read_be(index); + input.read_be(index2); + return new element_value_enum(type, index, index2, pool); + case CLASS: // Class + input.read_be(index); + return new element_value_class(type, index, pool); + case ANNOTATION: // Annotation + // FIXME: runtime visibility info needs to be passed from parent + return new element_value_annotation(ANNOTATION, annotation::read(input, pool), pool); + case ARRAY: // Array + input.read_be(index); + for (int i = 0; i < index; i++) + { + vals.push_back(element_value::readElementValue(input, pool)); + } + return new element_value_array(ARRAY, vals, pool); + default: + throw new java::classfile_exception(); + } +} +} \ No newline at end of file diff --git a/ultimmc/libraries/classparser/src/annotations.h b/ultimmc/libraries/classparser/src/annotations.h new file mode 100644 index 0000000..15bf05a --- /dev/null +++ b/ultimmc/libraries/classparser/src/annotations.h @@ -0,0 +1,278 @@ +#pragma once +#include "classfile.h" +#include +#include + +namespace java +{ +enum element_value_type : uint8_t +{ + INVALID = 0, + STRING = 's', + ENUM_CONSTANT = 'e', + CLASS = 'c', + ANNOTATION = '@', + ARRAY = '[', // one array dimension + PRIMITIVE_INT = 'I', // integer + PRIMITIVE_BYTE = 'B', // signed byte + PRIMITIVE_CHAR = 'C', // Unicode character code point in the Basic Multilingual Plane, + // encoded with UTF-16 + PRIMITIVE_DOUBLE = 'D', // double-precision floating-point value + PRIMITIVE_FLOAT = 'F', // single-precision floating-point value + PRIMITIVE_LONG = 'J', // long integer + PRIMITIVE_SHORT = 'S', // signed short + PRIMITIVE_BOOLEAN = 'Z' // true or false +}; +/** + * The element_value structure is a discriminated union representing the value of an + *element-value pair. + * It is used to represent element values in all attributes that describe annotations + * - RuntimeVisibleAnnotations + * - RuntimeInvisibleAnnotations + * - RuntimeVisibleParameterAnnotations + * - RuntimeInvisibleParameterAnnotations). + * + * The element_value structure has the following format: + */ +class element_value +{ +protected: + element_value_type type; + constant_pool &pool; + +public: + element_value(element_value_type type, constant_pool &pool) : type(type), pool(pool) {}; + virtual ~element_value() {} + + element_value_type getElementValueType() + { + return type; + } + + virtual std::string toString() = 0; + + static element_value *readElementValue(util::membuffer &input, constant_pool &pool); +}; + +/** + * Each value of the annotations table represents a single runtime-visible annotation on a + * program element. + * The annotation structure has the following format: + */ +class annotation +{ +public: + typedef std::vector> value_list; + +protected: + /** + * The value of the type_index item must be a valid index into the constant_pool table. + * The constant_pool entry at that index must be a CONSTANT_Utf8_info (§4.4.7) structure + * representing a field descriptor representing the annotation type corresponding + * to the annotation represented by this annotation structure. + */ + uint16_t type_index; + /** + * map between element_name_index and value. + * + * The value of the element_name_index item must be a valid index into the constant_pool + *table. + * The constant_pool entry at that index must be a CONSTANT_Utf8_info (§4.4.7) structure + *representing + * a valid field descriptor (§4.3.2) that denotes the name of the annotation type element + *represented + * by this element_value_pairs entry. + */ + value_list name_val_pairs; + /** + * Reference to the parent constant pool + */ + constant_pool &pool; + +public: + annotation(uint16_t type_index, constant_pool &pool) + : type_index(type_index), pool(pool) {}; + ~annotation() + { + for (unsigned i = 0; i < name_val_pairs.size(); i++) + { + delete name_val_pairs[i].second; + } + } + void add_pair(uint16_t key, element_value *value) + { + name_val_pairs.push_back(std::make_pair(key, value)); + } + ; + value_list::const_iterator begin() + { + return name_val_pairs.cbegin(); + } + value_list::const_iterator end() + { + return name_val_pairs.cend(); + } + std::string toString(); + static annotation *read(util::membuffer &input, constant_pool &pool); +}; +typedef std::vector annotation_table; + +/// type for simple value annotation elements +class element_value_simple : public element_value +{ +protected: + /// index of the constant in the constant pool + uint16_t index; + +public: + element_value_simple(element_value_type type, uint16_t index, constant_pool &pool) + : element_value(type, pool), index(index) { + // TODO: verify consistency + }; + uint16_t getIndex() + { + return index; + } + virtual std::string toString() + { + return pool[index].toString(); + } + ; +}; +/// The enum_const_value item is used if the tag item is 'e'. +class element_value_enum : public element_value +{ +protected: + /** + * The value of the type_name_index item must be a valid index into the constant_pool table. + * The constant_pool entry at that index must be a CONSTANT_Utf8_info (§4.4.7) structure + * representing a valid field descriptor (§4.3.2) that denotes the internal form of the + * binary + * name (§4.2.1) of the type of the enum constant represented by this element_value + * structure. + */ + uint16_t typeIndex; + /** + * The value of the const_name_index item must be a valid index into the constant_pool + * table. + * The constant_pool entry at that index must be a CONSTANT_Utf8_info (§4.4.7) structure + * representing the simple name of the enum constant represented by this element_value + * structure. + */ + uint16_t valueIndex; + +public: + element_value_enum(element_value_type type, uint16_t typeIndex, uint16_t valueIndex, + constant_pool &pool) + : element_value(type, pool), typeIndex(typeIndex), valueIndex(valueIndex) + { + // TODO: verify consistency + } + uint16_t getValueIndex() + { + return valueIndex; + } + uint16_t getTypeIndex() + { + return typeIndex; + } + virtual std::string toString() + { + return "enum value"; + } + ; +}; + +class element_value_class : public element_value +{ +protected: + /** + * The class_info_index item must be a valid index into the constant_pool table. + * The constant_pool entry at that index must be a CONSTANT_Utf8_info (§4.4.7) structure + * representing the return descriptor (§4.3.3) of the type that is reified by the class + * represented by this element_value structure. + * + * For example, 'V' for Void.class, 'Ljava/lang/Object;' for Object, etc. + * + * Or in plain english, you can store type information in annotations. Yay. + */ + uint16_t classIndex; + +public: + element_value_class(element_value_type type, uint16_t classIndex, constant_pool &pool) + : element_value(type, pool), classIndex(classIndex) + { + // TODO: verify consistency + } + uint16_t getIndex() + { + return classIndex; + } + virtual std::string toString() + { + return "class"; + } + ; +}; + +/// nested annotations... yay +class element_value_annotation : public element_value +{ +private: + annotation *nestedAnnotation; + +public: + element_value_annotation(element_value_type type, annotation *nestedAnnotation, + constant_pool &pool) + : element_value(type, pool), nestedAnnotation(nestedAnnotation) {}; + ~element_value_annotation() + { + if (nestedAnnotation) + { + delete nestedAnnotation; + nestedAnnotation = nullptr; + } + } + virtual std::string toString() + { + return "nested annotation"; + } + ; +}; + +/// and arrays! +class element_value_array : public element_value +{ +public: + typedef std::vector elem_vec; + +protected: + elem_vec values; + +public: + element_value_array(element_value_type type, std::vector &values, + constant_pool &pool) + : element_value(type, pool), values(values) {}; + ~element_value_array() + { + for (unsigned i = 0; i < values.size(); i++) + { + delete values[i]; + } + } + ; + elem_vec::const_iterator begin() + { + return values.cbegin(); + } + elem_vec::const_iterator end() + { + return values.cend(); + } + virtual std::string toString() + { + return "array"; + } + ; +}; +} \ No newline at end of file diff --git a/ultimmc/libraries/classparser/src/classfile.h b/ultimmc/libraries/classparser/src/classfile.h new file mode 100644 index 0000000..1616a82 --- /dev/null +++ b/ultimmc/libraries/classparser/src/classfile.h @@ -0,0 +1,156 @@ +#pragma once +#include "membuffer.h" +#include "constants.h" +#include "annotations.h" +#include +namespace java +{ +/** + * Class representing a Java .class file + */ +class classfile : public util::membuffer +{ +public: + classfile(char *data, std::size_t size) : membuffer(data, size) + { + valid = false; + is_synthetic = false; + read_be(magic); + if (magic != 0xCAFEBABE) + throw new classfile_exception(); + read_be(minor_version); + read_be(major_version); + constants.load(*this); + read_be(access_flags); + read_be(this_class); + read_be(super_class); + + // Interfaces + uint16_t iface_count = 0; + read_be(iface_count); + while (iface_count) + { + uint16_t iface; + read_be(iface); + interfaces.push_back(iface); + iface_count--; + } + + // Fields + // read fields (and attributes from inside fields) (and possible inner classes. yay for + // recursion!) + // for now though, we will ignore all attributes + /* + * field_info + * { + * u2 access_flags; + * u2 name_index; + * u2 descriptor_index; + * u2 attributes_count; + * attribute_info attributes[attributes_count]; + * } + */ + uint16_t field_count = 0; + read_be(field_count); + while (field_count) + { + // skip field stuff + skip(6); + // and skip field attributes + uint16_t attr_count = 0; + read_be(attr_count); + while (attr_count) + { + skip(2); + uint32_t attr_length = 0; + read_be(attr_length); + skip(attr_length); + attr_count--; + } + field_count--; + } + + // class methods + /* + * method_info + * { + * u2 access_flags; + * u2 name_index; + * u2 descriptor_index; + * u2 attributes_count; + * attribute_info attributes[attributes_count]; + * } + */ + uint16_t method_count = 0; + read_be(method_count); + while (method_count) + { + skip(6); + // and skip method attributes + uint16_t attr_count = 0; + read_be(attr_count); + while (attr_count) + { + skip(2); + uint32_t attr_length = 0; + read_be(attr_length); + skip(attr_length); + attr_count--; + } + method_count--; + } + + // class attributes + // there are many kinds of attributes. this is just the generic wrapper structure. + // type is decided by attribute name. extensions to the standard are *possible* + // class annotations are one kind of a attribute (one per class) + /* + * attribute_info + * { + * u2 attribute_name_index; + * u4 attribute_length; + * u1 info[attribute_length]; + * } + */ + uint16_t class_attr_count = 0; + read_be(class_attr_count); + while (class_attr_count) + { + uint16_t name_idx = 0; + read_be(name_idx); + uint32_t attr_length = 0; + read_be(attr_length); + + auto name = constants[name_idx]; + if (name.str_data == "RuntimeVisibleAnnotations") + { + uint16_t num_annotations = 0; + read_be(num_annotations); + while (num_annotations) + { + visible_class_annotations.push_back(annotation::read(*this, constants)); + num_annotations--; + } + } + else + skip(attr_length); + class_attr_count--; + } + valid = true; + } + ; + bool valid; + bool is_synthetic; + uint32_t magic; + uint16_t minor_version; + uint16_t major_version; + constant_pool constants; + uint16_t access_flags; + uint16_t this_class; + uint16_t super_class; + // interfaces this class implements ? must be. investigate. + std::vector interfaces; + // FIXME: doesn't free up memory on delete + java::annotation_table visible_class_annotations; +}; +} \ No newline at end of file diff --git a/ultimmc/libraries/classparser/src/classparser.cpp b/ultimmc/libraries/classparser/src/classparser.cpp new file mode 100644 index 0000000..8825ea3 --- /dev/null +++ b/ultimmc/libraries/classparser/src/classparser.cpp @@ -0,0 +1,83 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Authors: Orochimarufan + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "classfile.h" +#include "classparser.h" + +#include +#include +#include + +namespace classparser +{ + +QString GetMinecraftJarVersion(QString jarName) +{ + QString version; + + // check if minecraft.jar exists + QFile jar(jarName); + if (!jar.exists()) + return version; + + // open minecraft.jar + QuaZip zip(&jar); + if (!zip.open(QuaZip::mdUnzip)) + return version; + + // open Minecraft.class + zip.setCurrentFile("net/minecraft/client/Minecraft.class", QuaZip::csSensitive); + QuaZipFile Minecraft(&zip); + if (!Minecraft.open(QuaZipFile::ReadOnly)) + return version; + + // read Minecraft.class + qint64 size = Minecraft.size(); + char *classfile = new char[size]; + Minecraft.read(classfile, size); + + // parse Minecraft.class + try + { + char *temp = classfile; + java::classfile MinecraftClass(temp, size); + java::constant_pool constants = MinecraftClass.constants; + for (java::constant_pool::container_type::const_iterator iter = constants.begin(); + iter != constants.end(); iter++) + { + const java::constant &constant = *iter; + if (constant.type != java::constant_type_t::j_string_data) + continue; + const std::string &str = constant.str_data; + qDebug() << QString::fromStdString(str); + if (str.compare(0, 20, "Minecraft Minecraft ") == 0) + { + version = str.substr(20).data(); + break; + } + } + } + catch (const java::classfile_exception &) { } + + // clean up + delete[] classfile; + Minecraft.close(); + zip.close(); + jar.close(); + + return version; +} +} diff --git a/ultimmc/libraries/classparser/src/constants.h b/ultimmc/libraries/classparser/src/constants.h new file mode 100644 index 0000000..3b6c3b7 --- /dev/null +++ b/ultimmc/libraries/classparser/src/constants.h @@ -0,0 +1,231 @@ +#pragma once +#include "errors.h" +#include + +namespace java +{ +enum class constant_type_t : uint8_t +{ + j_hole = 0, // HACK: this is a hole in the array, because java is crazy + j_string_data = 1, + j_int = 3, + j_float = 4, + j_long = 5, + j_double = 6, + j_class = 7, + j_string = 8, + j_fieldref = 9, + j_methodref = 10, + j_interface_methodref = 11, + j_nameandtype = 12 + // FIXME: missing some constant types, see https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4 +}; + +struct ref_type_t +{ + /** + * Class reference: + * an index within the constant pool to a UTF-8 string containing + * the fully qualified class name (in internal format) + * Used for j_class, j_fieldref, j_methodref and j_interface_methodref + */ + uint16_t class_idx; + // used for j_fieldref, j_methodref and j_interface_methodref + uint16_t name_and_type_idx; +}; + +struct name_and_type_t +{ + uint16_t name_index; + uint16_t descriptor_index; +}; + +class constant +{ +public: + constant_type_t type = constant_type_t::j_hole; + + constant(util::membuffer &buf) + { + buf.read(type); + + // load data depending on type + switch (type) + { + case constant_type_t::j_float: + buf.read_be(data.int_data); + break; + case constant_type_t::j_int: + buf.read_be(data.int_data); // same as float data really + break; + case constant_type_t::j_double: + buf.read_be(data.long_data); + break; + case constant_type_t::j_long: + buf.read_be(data.long_data); // same as double + break; + case constant_type_t::j_class: + buf.read_be(data.ref_type.class_idx); + break; + case constant_type_t::j_fieldref: + case constant_type_t::j_methodref: + case constant_type_t::j_interface_methodref: + buf.read_be(data.ref_type.class_idx); + buf.read_be(data.ref_type.name_and_type_idx); + break; + case constant_type_t::j_string: + buf.read_be(data.index); + break; + case constant_type_t::j_string_data: + // HACK HACK: for now, we call these UTF-8 and do no further processing. + // Later, we should do some decoding. It's really modified UTF-8 + // * U+0000 is represented as 0xC0,0x80 invalid character + // * any single zero byte ends the string + // * characters above U+10000 are encoded like in CESU-8 + buf.read_jstr(str_data); + break; + case constant_type_t::j_nameandtype: + buf.read_be(data.name_and_type.name_index); + buf.read_be(data.name_and_type.descriptor_index); + break; + default: + // invalid constant type! + throw new classfile_exception(); + } + } + constant(int) + { + } + + std::string toString() + { + std::ostringstream ss; + switch (type) + { + case constant_type_t::j_hole: + ss << "Fake legacy entry"; + break; + case constant_type_t::j_float: + ss << "Float: " << data.float_data; + break; + case constant_type_t::j_double: + ss << "Double: " << data.double_data; + break; + case constant_type_t::j_int: + ss << "Int: " << data.int_data; + break; + case constant_type_t::j_long: + ss << "Long: " << data.long_data; + break; + case constant_type_t::j_string_data: + ss << "StrData: " << str_data; + break; + case constant_type_t::j_string: + ss << "Str: " << data.index; + break; + case constant_type_t::j_fieldref: + ss << "FieldRef: " << data.ref_type.class_idx << " " << data.ref_type.name_and_type_idx; + break; + case constant_type_t::j_methodref: + ss << "MethodRef: " << data.ref_type.class_idx << " " << data.ref_type.name_and_type_idx; + break; + case constant_type_t::j_interface_methodref: + ss << "IfMethodRef: " << data.ref_type.class_idx << " " << data.ref_type.name_and_type_idx; + break; + case constant_type_t::j_class: + ss << "Class: " << data.ref_type.class_idx; + break; + case constant_type_t::j_nameandtype: + ss << "NameAndType: " << data.name_and_type.name_index << " " + << data.name_and_type.descriptor_index; + break; + default: + ss << "Invalid entry (" << int(type) << ")"; + break; + } + return ss.str(); + } + + std::string str_data; /** String data in 'modified utf-8'.*/ + + // store everything here. + union + { + int32_t int_data; + int64_t long_data; + float float_data; + double double_data; + uint16_t index; + ref_type_t ref_type; + name_and_type_t name_and_type; + } data = {0}; +}; + +/** + * A helper class that represents the custom container used in Java class file for storage of + * constants + */ +class constant_pool +{ +public: + /** + * Create a pool of constants + */ + constant_pool() + { + } + /** + * Load a java constant pool + */ + void load(util::membuffer &buf) + { + // FIXME: @SANITY this should check for the end of buffer. + uint16_t length = 0; + buf.read_be(length); + length--; + const constant *last_constant = nullptr; + while (length) + { + const constant &cnst = constant(buf); + constants.push_back(cnst); + last_constant = &constants[constants.size() - 1]; + if (last_constant->type == constant_type_t::j_double || + last_constant->type == constant_type_t::j_long) + { + // push in a fake constant to preserve indexing + constants.push_back(constant(0)); + length -= 2; + } + else + { + length--; + } + } + } + typedef std::vector container_type; + /** + * Access constants based on jar file index numbers (index of the first element is 1) + */ + java::constant &operator[](std::size_t constant_index) + { + if (constant_index == 0 || constant_index > constants.size()) + { + throw new classfile_exception(); + } + return constants[constant_index - 1]; + } + ; + container_type::const_iterator begin() const + { + return constants.begin(); + } + ; + container_type::const_iterator end() const + { + return constants.end(); + } + +private: + container_type constants; +}; +} diff --git a/ultimmc/libraries/classparser/src/errors.h b/ultimmc/libraries/classparser/src/errors.h new file mode 100644 index 0000000..ddbbd82 --- /dev/null +++ b/ultimmc/libraries/classparser/src/errors.h @@ -0,0 +1,8 @@ +#pragma once +#include +namespace java +{ +class classfile_exception : public std::exception +{ +}; +} diff --git a/ultimmc/libraries/classparser/src/javaendian.h b/ultimmc/libraries/classparser/src/javaendian.h new file mode 100644 index 0000000..5a6e107 --- /dev/null +++ b/ultimmc/libraries/classparser/src/javaendian.h @@ -0,0 +1,59 @@ +#pragma once +#include + +/** + * Swap bytes between big endian and local number representation + */ +namespace util +{ +#ifdef MULTIMC_BIG_ENDIAN +inline uint64_t bigswap(uint64_t x) +{ + return x; +} + +inline uint32_t bigswap(uint32_t x) +{ + return x; +} + +inline uint16_t bigswap(uint16_t x) +{ + return x; +} + +#else +inline uint64_t bigswap(uint64_t x) +{ + return (x >> 56) | ((x << 40) & 0x00FF000000000000) | ((x << 24) & 0x0000FF0000000000) | + ((x << 8) & 0x000000FF00000000) | ((x >> 8) & 0x00000000FF000000) | + ((x >> 24) & 0x0000000000FF0000) | ((x >> 40) & 0x000000000000FF00) | (x << 56); +} + +inline uint32_t bigswap(uint32_t x) +{ + return (x >> 24) | ((x << 8) & 0x00FF0000) | ((x >> 8) & 0x0000FF00) | (x << 24); +} + +inline uint16_t bigswap(uint16_t x) +{ + return (x >> 8) | (x << 8); +} + +#endif + +inline int64_t bigswap(int64_t x) +{ + return static_cast(bigswap(static_cast(x))); +} + +inline int32_t bigswap(int32_t x) +{ + return static_cast(bigswap(static_cast(x))); +} + +inline int16_t bigswap(int16_t x) +{ + return static_cast(bigswap(static_cast(x))); +} +} diff --git a/ultimmc/libraries/classparser/src/membuffer.h b/ultimmc/libraries/classparser/src/membuffer.h new file mode 100644 index 0000000..f81c970 --- /dev/null +++ b/ultimmc/libraries/classparser/src/membuffer.h @@ -0,0 +1,63 @@ +#pragma once +#include +#include +#include +#include +#include "javaendian.h" + +namespace util +{ +class membuffer +{ +public: + membuffer(char *buffer, std::size_t size) + { + current = start = buffer; + end = start + size; + } + ~membuffer() + { + // maybe? possibly? left out to avoid confusion. for now. + // delete start; + } + /** + * Read some value. That's all ;) + */ + template void read(T &val) + { + val = *(T *)current; + current += sizeof(T); + } + /** + * Read a big-endian number + * valid for 2-byte, 4-byte and 8-byte variables + */ + template void read_be(T &val) + { + val = util::bigswap(*(T *)current); + current += sizeof(T); + } + /** + * Read a string in the format: + * 2B length (big endian, unsigned) + * length bytes data + */ + void read_jstr(std::string &str) + { + uint16_t length = 0; + read_be(length); + str.append(current, length); + current += length; + } + /** + * Skip N bytes + */ + void skip(std::size_t N) + { + current += N; + } + +private: + char *start, *end, *current; +}; +} diff --git a/ultimmc/libraries/ganalytics/CMakeLists.txt b/ultimmc/libraries/ganalytics/CMakeLists.txt new file mode 100644 index 0000000..cf1cc55 --- /dev/null +++ b/ultimmc/libraries/ganalytics/CMakeLists.txt @@ -0,0 +1,17 @@ +project(ganalytics) + +find_package(Qt5Core) +find_package(Qt5Gui) +find_package(Qt5Network) + +set(ganalytics_SOURCES +src/ganalytics.cpp +src/ganalytics_worker.cpp +src/ganalytics_worker.h +include/ganalytics.h +) + +add_library(ganalytics STATIC ${ganalytics_SOURCES}) +target_link_libraries(ganalytics Qt5::Core Qt5::Gui Qt5::Network) +target_include_directories(ganalytics PUBLIC include) +target_link_libraries(ganalytics systeminfo) diff --git a/ultimmc/libraries/ganalytics/LICENSE.txt b/ultimmc/libraries/ganalytics/LICENSE.txt new file mode 100644 index 0000000..795497f --- /dev/null +++ b/ultimmc/libraries/ganalytics/LICENSE.txt @@ -0,0 +1,24 @@ +Copyright (c) 2014-2015, University of Applied Sciences Augsburg +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the University of Applied Sciences Augsburg nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS +BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +OODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +UT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ultimmc/libraries/ganalytics/README.md b/ultimmc/libraries/ganalytics/README.md new file mode 100644 index 0000000..d7e1e33 --- /dev/null +++ b/ultimmc/libraries/ganalytics/README.md @@ -0,0 +1,34 @@ +qt-google-analytics +================ + +Qt5 classes for providing google analytics usage in a Qt/QML application. + +## Building +Include ```qt-google-analytics.pri``` in your .pro file. + +## Using +Please make sure you have set your application information using ```QApplication::setApplicationName``` and ```QApplication::setApplicationVersion```. + +### In C++: +``` +GAnalytics tracker("UA-my-id"); +tracker.sendScreenView("Main Screen"); +``` + +### In QtQuick: +Register the class on the C++ side using ```qmlRegisterType("analytics", 0, 1, "Tracker");``` +``` +Tracker { + id: tracker + trackingID: "UA-my-id" +} + +[...] +tracker.sendScreenView("Main Screen") +``` + +There is also an example application in the examples folder. + +## License +Copyright (c) 2014-2016, University of Applied Sciences Augsburg. +All rights reserved. Distributed under the terms and conditions of the BSD License. See separate LICENSE.txt. diff --git a/ultimmc/libraries/ganalytics/include/ganalytics.h b/ultimmc/libraries/ganalytics/include/ganalytics.h new file mode 100644 index 0000000..ba42245 --- /dev/null +++ b/ultimmc/libraries/ganalytics/include/ganalytics.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include + +class QNetworkAccessManager; +class GAnalyticsWorker; + +class GAnalytics : public QObject +{ + Q_OBJECT + Q_ENUMS(LogLevel) + +public: + explicit GAnalytics(const QString &trackingID, const QString &clientID, const int version, QObject *parent = 0); + ~GAnalytics(); + +public: + enum LogLevel + { + Debug, + Info, + Error + }; + + int version(); + + void setLogLevel(LogLevel logLevel); + LogLevel logLevel() const; + + // Getter and Setters + void setViewportSize(const QString &viewportSize); + QString viewportSize() const; + + void setLanguage(const QString &language); + QString language() const; + + void setAnonymizeIPs(bool anonymize); + bool anonymizeIPs(); + + void setSendInterval(int milliseconds); + int sendInterval() const; + + void enable(bool state = true); + bool isEnabled(); + + /// Get or set the network access manager. If none is set, the class creates its own on the first request + void setNetworkAccessManager(QNetworkAccessManager *networkAccessManager); + QNetworkAccessManager *networkAccessManager() const; + +public slots: + void sendScreenView(const QString &screenName, const QVariantMap &customValues = QVariantMap()); + void sendEvent(const QString &category, const QString &action, const QString &label = QString(), const QVariant &value = QVariant(), + const QVariantMap &customValues = QVariantMap()); + void sendException(const QString &exceptionDescription, bool exceptionFatal = true, const QVariantMap &customValues = QVariantMap()); + void startSession(); + void endSession(); + +private: + GAnalyticsWorker *d; + + friend QDataStream &operator<<(QDataStream &outStream, const GAnalytics &analytics); + friend QDataStream &operator>>(QDataStream &inStream, GAnalytics &analytics); +}; + +QDataStream &operator<<(QDataStream &outStream, const GAnalytics &analytics); +QDataStream &operator>>(QDataStream &inStream, GAnalytics &analytics); diff --git a/ultimmc/libraries/ganalytics/src/ganalytics.cpp b/ultimmc/libraries/ganalytics/src/ganalytics.cpp new file mode 100644 index 0000000..a4b7394 --- /dev/null +++ b/ultimmc/libraries/ganalytics/src/ganalytics.cpp @@ -0,0 +1,237 @@ +#include "ganalytics.h" +#include "ganalytics_worker.h" +#include "sys.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +GAnalytics::GAnalytics(const QString &trackingID, const QString &clientID, const int version, QObject *parent) : QObject(parent) +{ + d = new GAnalyticsWorker(this); + d->m_trackingID = trackingID; + d->m_clientID = clientID; + d->m_version = version; +} + +/** + * Destructor of class GAnalytics. + */ +GAnalytics::~GAnalytics() +{ + delete d; +} + +void GAnalytics::setLogLevel(GAnalytics::LogLevel logLevel) +{ + d->m_logLevel = logLevel; +} + +GAnalytics::LogLevel GAnalytics::logLevel() const +{ + return d->m_logLevel; +} + +// SETTER and GETTER +void GAnalytics::setViewportSize(const QString &viewportSize) +{ + d->m_viewportSize = viewportSize; +} + +QString GAnalytics::viewportSize() const +{ + return d->m_viewportSize; +} + +void GAnalytics::setLanguage(const QString &language) +{ + d->m_language = language; +} + +QString GAnalytics::language() const +{ + return d->m_language; +} + +void GAnalytics::setAnonymizeIPs(bool anonymize) +{ + d->m_anonymizeIPs = anonymize; +} + +bool GAnalytics::anonymizeIPs() +{ + return d->m_anonymizeIPs; +} + +void GAnalytics::setSendInterval(int milliseconds) +{ + d->m_timer.setInterval(milliseconds); +} + +int GAnalytics::sendInterval() const +{ + return (d->m_timer.interval()); +} + +bool GAnalytics::isEnabled() +{ + return d->m_isEnabled; +} + +void GAnalytics::enable(bool state) +{ + d->enable(state); +} + +int GAnalytics::version() +{ + return d->m_version; +} + +void GAnalytics::setNetworkAccessManager(QNetworkAccessManager *networkAccessManager) +{ + if (d->networkManager != networkAccessManager) + { + // Delete the old network manager if it was our child + if (d->networkManager && d->networkManager->parent() == this) + { + d->networkManager->deleteLater(); + } + + d->networkManager = networkAccessManager; + } +} + +QNetworkAccessManager *GAnalytics::networkAccessManager() const +{ + return d->networkManager; +} + +static void appendCustomValues(QUrlQuery &query, const QVariantMap &customValues) +{ + for (QVariantMap::const_iterator iter = customValues.begin(); iter != customValues.end(); ++iter) + { + query.addQueryItem(iter.key(), iter.value().toString()); + } +} + +/** + * Sent screen view is called when the user changed the applications view. + * These action of the user should be noticed and reported. Therefore + * a QUrlQuery is build in this method. It holts all the parameter for + * a http POST. The UrlQuery will be stored in a message Queue. + */ +void GAnalytics::sendScreenView(const QString &screenName, const QVariantMap &customValues) +{ + d->logMessage(Info, QString("ScreenView: %1").arg(screenName)); + + QUrlQuery query = d->buildStandardPostQuery("screenview"); + query.addQueryItem("cd", screenName); + query.addQueryItem("an", d->m_appName); + query.addQueryItem("av", d->m_appVersion); + appendCustomValues(query, customValues); + + d->enqueQueryWithCurrentTime(query); +} + +/** + * This method is called whenever a button was pressed in the application. + * A query for a POST message will be created to report this event. The + * created query will be stored in a message queue. + */ +void GAnalytics::sendEvent(const QString &category, const QString &action, const QString &label, const QVariant &value, const QVariantMap &customValues) +{ + QUrlQuery query = d->buildStandardPostQuery("event"); + query.addQueryItem("an", d->m_appName); + query.addQueryItem("av", d->m_appVersion); + query.addQueryItem("ec", category); + query.addQueryItem("ea", action); + if (!label.isEmpty()) + query.addQueryItem("el", label); + if (value.isValid()) + query.addQueryItem("ev", value.toString()); + + appendCustomValues(query, customValues); + + d->enqueQueryWithCurrentTime(query); +} + +/** + * Method is called after an exception was raised. It builds a + * query for a POST message. These query will be stored in a + * message queue. + */ +void GAnalytics::sendException(const QString &exceptionDescription, bool exceptionFatal, const QVariantMap &customValues) +{ + QUrlQuery query = d->buildStandardPostQuery("exception"); + query.addQueryItem("an", d->m_appName); + query.addQueryItem("av", d->m_appVersion); + + query.addQueryItem("exd", exceptionDescription); + + if (exceptionFatal) + { + query.addQueryItem("exf", "1"); + } + else + { + query.addQueryItem("exf", "0"); + } + appendCustomValues(query, customValues); + + d->enqueQueryWithCurrentTime(query); +} + +/** + * Session starts. This event will be sent by a POST message. + * Query is setup in this method and stored in the message + * queue. + */ +void GAnalytics::startSession() +{ + QVariantMap customValues; + customValues.insert("sc", "start"); + sendEvent("Session", "Start", QString(), QVariant(), customValues); +} + +/** + * Session ends. This event will be sent by a POST message. + * Query is setup in this method and stored in the message + * queue. + */ +void GAnalytics::endSession() +{ + QVariantMap customValues; + customValues.insert("sc", "end"); + sendEvent("Session", "End", QString(), QVariant(), customValues); +} + +/** + * Qut stream to persist class GAnalytics. + */ +QDataStream &operator<<(QDataStream &outStream, const GAnalytics &analytics) +{ + outStream << analytics.d->persistMessageQueue(); + + return outStream; +} + +/** + * In stream to read GAnalytics from file. + */ +QDataStream &operator>>(QDataStream &inStream, GAnalytics &analytics) +{ + QList dataList; + inStream >> dataList; + analytics.d->readMessagesFromFile(dataList); + + return inStream; +} diff --git a/ultimmc/libraries/ganalytics/src/ganalytics_worker.cpp b/ultimmc/libraries/ganalytics/src/ganalytics_worker.cpp new file mode 100644 index 0000000..b0ae75a --- /dev/null +++ b/ultimmc/libraries/ganalytics/src/ganalytics_worker.cpp @@ -0,0 +1,254 @@ +#include "ganalytics.h" +#include "ganalytics_worker.h" +#include "sys.h" + +#include +#include +#include + +#include +#include + +const QLatin1String GAnalyticsWorker::dateTimeFormat("yyyy,MM,dd-hh:mm::ss:zzz"); + +GAnalyticsWorker::GAnalyticsWorker(GAnalytics *parent) + : QObject(parent), q(parent), m_logLevel(GAnalytics::Error) +{ + m_appName = QCoreApplication::instance()->applicationName(); + m_appVersion = QCoreApplication::instance()->applicationVersion(); + m_request.setUrl(QUrl("https://www.google-analytics.com/collect")); + m_request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + m_request.setHeader(QNetworkRequest::UserAgentHeader, getUserAgent()); + + m_language = QLocale::system().name().toLower().replace("_", "-"); + m_screenResolution = getScreenResolution(); + + m_timer.setInterval(m_timerInterval); + connect(&m_timer, &QTimer::timeout, this, &GAnalyticsWorker::postMessage); +} + +void GAnalyticsWorker::enable(bool state) +{ + // state change to the same is not valid. + if(m_isEnabled == state) + { + return; + } + + m_isEnabled = state; + if(m_isEnabled) + { + // enable -> start doing things :) + m_timer.start(); + } + else + { + // disable -> stop the timer + m_timer.stop(); + } +} + +void GAnalyticsWorker::logMessage(GAnalytics::LogLevel level, const QString &message) +{ + if (m_logLevel > level) + { + return; + } + + qDebug() << "[Analytics]" << message; +} + +/** + * Build the POST query. Adds all parameter to the query + * which are used in every POST. + * @param type Type of POST message. The event which is to post. + * @return query Most used parameter in a query for a POST. + */ +QUrlQuery GAnalyticsWorker::buildStandardPostQuery(const QString &type) +{ + QUrlQuery query; + query.addQueryItem("v", "1"); + query.addQueryItem("tid", m_trackingID); + query.addQueryItem("cid", m_clientID); + if (!m_userID.isEmpty()) + { + query.addQueryItem("uid", m_userID); + } + query.addQueryItem("t", type); + query.addQueryItem("ul", m_language); + query.addQueryItem("vp", m_viewportSize); + query.addQueryItem("sr", m_screenResolution); + if(m_anonymizeIPs) + { + query.addQueryItem("aip", "1"); + } + return query; +} + +/** + * Get primary screen resolution. + * @return A QString like "800x600". + */ +QString GAnalyticsWorker::getScreenResolution() +{ + QScreen *screen = QGuiApplication::primaryScreen(); + QSize size = screen->size(); + + return QString("%1x%2").arg(size.width()).arg(size.height()); +} + +/** + * Try to gain information about the system where this application + * is running. It needs to get the name and version of the operating + * system, the language and screen resolution. + * All this information will be send in POST messages. + * @return agent A QString with all the information formatted for a POST message. + */ +QString GAnalyticsWorker::getUserAgent() +{ + return QString("%1/%2").arg(m_appName).arg(m_appVersion); +} + +/** + * The message queue contains a list of QueryBuffer object. + * QueryBuffer holds a QUrlQuery object and a QDateTime object. + * These both object are freed from the buffer object and + * inserted as QString objects in a QList. + * @return dataList The list with concartinated queue data. + */ +QList GAnalyticsWorker::persistMessageQueue() +{ + QList dataList; + foreach (QueryBuffer buffer, m_messageQueue) + { + dataList << buffer.postQuery.toString(); + dataList << buffer.time.toString(dateTimeFormat); + } + return dataList; +} + +/** + * Reads persistent messages from a file. + * Gets all message data as a QList. + * Two lines in the list build a QueryBuffer object. + */ +void GAnalyticsWorker::readMessagesFromFile(const QList &dataList) +{ + QListIterator iter(dataList); + while (iter.hasNext()) + { + QString queryString = iter.next(); + QString dateString = iter.next(); + QUrlQuery query; + query.setQuery(queryString); + QDateTime dateTime = QDateTime::fromString(dateString, dateTimeFormat); + QueryBuffer buffer; + buffer.postQuery = query; + buffer.time = dateTime; + m_messageQueue.enqueue(buffer); + } +} + +/** + * Takes a QUrlQuery object and wrapp it together with + * a QTime object into a QueryBuffer struct. These struct + * will be stored in the message queue. + */ +void GAnalyticsWorker::enqueQueryWithCurrentTime(const QUrlQuery &query) +{ + QueryBuffer buffer; + buffer.postQuery = query; + buffer.time = QDateTime::currentDateTime(); + + m_messageQueue.enqueue(buffer); +} + +/** + * This function is called by a timer interval. + * The function tries to send a messages from the queue. + * If message was successfully send then this function + * will be called back to send next message. + * If message queue contains more than one message then + * the connection will kept open. + * The message POST is asyncroniously when the server + * answered a signal will be emitted. + */ +void GAnalyticsWorker::postMessage() +{ + if (m_messageQueue.isEmpty()) + { + // queue empty -> try sending later + m_timer.start(); + return; + } + else + { + // queue has messages -> stop timer and start sending + m_timer.stop(); + } + + QString connection = "close"; + if (m_messageQueue.count() > 1) + { + connection = "keep-alive"; + } + + QueryBuffer buffer = m_messageQueue.head(); + QDateTime sendTime = QDateTime::currentDateTime(); + qint64 timeDiff = buffer.time.msecsTo(sendTime); + + if (timeDiff > fourHours) + { + // too old. + m_messageQueue.dequeue(); + emit postMessage(); + return; + } + + buffer.postQuery.addQueryItem("qt", QString::number(timeDiff)); + m_request.setRawHeader("Connection", connection.toUtf8()); + m_request.setHeader(QNetworkRequest::ContentLengthHeader, buffer.postQuery.toString().length()); + + logMessage(GAnalytics::Debug, "Query string = " + buffer.postQuery.toString()); + + // Create a new network access manager if we don't have one yet + if (networkManager == NULL) + { + networkManager = new QNetworkAccessManager(this); + } + + QNetworkReply *reply = networkManager->post(m_request, buffer.postQuery.query(QUrl::EncodeUnicode).toUtf8()); + connect(reply, SIGNAL(finished()), this, SLOT(postMessageFinished())); +} + +/** + * NetworkAccsessManager has finished to POST a message. + * If POST message was successfully send then the message + * query should be removed from queue. + * SIGNAL "postMessage" will be emitted to send next message + * if there is any. + * If message couldn't be send then next try is when the + * timer emits its signal. + */ +void GAnalyticsWorker::postMessageFinished() +{ + QNetworkReply *reply = qobject_cast(sender()); + + int httpStausCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (httpStausCode < 200 || httpStausCode > 299) + { + logMessage(GAnalytics::Error, QString("Error posting message: %1").arg(reply->errorString())); + + // An error ocurred. Try sending later. + m_timer.start(); + return; + } + else + { + logMessage(GAnalytics::Debug, "Message sent"); + } + + m_messageQueue.dequeue(); + postMessage(); + reply->deleteLater(); +} diff --git a/ultimmc/libraries/ganalytics/src/ganalytics_worker.h b/ultimmc/libraries/ganalytics/src/ganalytics_worker.h new file mode 100644 index 0000000..1962f79 --- /dev/null +++ b/ultimmc/libraries/ganalytics/src/ganalytics_worker.h @@ -0,0 +1,65 @@ +#pragma once + +#include +#include +#include +#include +#include + +struct QueryBuffer +{ + QUrlQuery postQuery; + QDateTime time; +}; + +class GAnalyticsWorker : public QObject +{ + Q_OBJECT + +public: + explicit GAnalyticsWorker(GAnalytics *parent = 0); + + GAnalytics *q; + + QNetworkAccessManager *networkManager = nullptr; + + QQueue m_messageQueue; + QTimer m_timer; + QNetworkRequest m_request; + GAnalytics::LogLevel m_logLevel; + + QString m_trackingID; + QString m_clientID; + QString m_userID; + QString m_appName; + QString m_appVersion; + QString m_language; + QString m_screenResolution; + QString m_viewportSize; + + bool m_anonymizeIPs = false; + bool m_isEnabled = false; + int m_timerInterval = 30000; + int m_version = 0; + + const static int fourHours = 4 * 60 * 60 * 1000; + const static QLatin1String dateTimeFormat; + +public: + void logMessage(GAnalytics::LogLevel level, const QString &message); + + QUrlQuery buildStandardPostQuery(const QString &type); + QString getScreenResolution(); + QString getUserAgent(); + QList persistMessageQueue(); + void readMessagesFromFile(const QList &dataList); + + void enqueQueryWithCurrentTime(const QUrlQuery &query); + void setIsSending(bool doSend); + void enable(bool state); + +public slots: + void postMessage(); + void postMessageFinished(); +}; + diff --git a/ultimmc/libraries/hoedown/CMakeLists.txt b/ultimmc/libraries/hoedown/CMakeLists.txt new file mode 100644 index 0000000..7902e73 --- /dev/null +++ b/ultimmc/libraries/hoedown/CMakeLists.txt @@ -0,0 +1,26 @@ +# hoedown 3.0.2 - https://github.com/hoedown/hoedown/archive/3.0.2.tar.gz +project(hoedown LANGUAGES C VERSION 3.0.2) + +set(HOEDOWN_SOURCES +include/hoedown/autolink.h +include/hoedown/buffer.h +include/hoedown/document.h +include/hoedown/escape.h +include/hoedown/html.h +include/hoedown/stack.h +include/hoedown/version.h +src/autolink.c +src/buffer.c +src/document.c +src/escape.c +src/html.c +src/html_blocks.c +src/html_smartypants.c +src/stack.c +src/version.c +) + +# Include self. +add_library(hoedown STATIC ${HOEDOWN_SOURCES}) + +target_include_directories(hoedown PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) diff --git a/ultimmc/libraries/hoedown/LICENSE b/ultimmc/libraries/hoedown/LICENSE new file mode 100644 index 0000000..4e75de4 --- /dev/null +++ b/ultimmc/libraries/hoedown/LICENSE @@ -0,0 +1,15 @@ +Copyright (c) 2008, Natacha Porté +Copyright (c) 2011, Vicent Martí +Copyright (c) 2014, Xavier Mendez, Devin Torres and the Hoedown authors + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/ultimmc/libraries/hoedown/README.md b/ultimmc/libraries/hoedown/README.md new file mode 100644 index 0000000..abe2b6c --- /dev/null +++ b/ultimmc/libraries/hoedown/README.md @@ -0,0 +1,9 @@ +Hoedown +======= + +This is Hoedown 3.0.2, taken from [the hoedown github repo](https://github.com/hoedown/hoedown). + +`Hoedown` is a revived fork of [Sundown](https://github.com/vmg/sundown), +the Markdown parser based on the original code of the +[Upskirt library](http://fossil.instinctive.eu/libupskirt/index) +by Natacha Porté. diff --git a/ultimmc/libraries/hoedown/include/hoedown/autolink.h b/ultimmc/libraries/hoedown/include/hoedown/autolink.h new file mode 100644 index 0000000..953e780 --- /dev/null +++ b/ultimmc/libraries/hoedown/include/hoedown/autolink.h @@ -0,0 +1,46 @@ +/* autolink.h - versatile autolinker */ + +#ifndef HOEDOWN_AUTOLINK_H +#define HOEDOWN_AUTOLINK_H + +#include "buffer.h" + +#ifdef __cplusplus +extern "C" { +#endif + + +/************* + * CONSTANTS * + *************/ + +typedef enum hoedown_autolink_flags { + HOEDOWN_AUTOLINK_SHORT_DOMAINS = (1 << 0) +} hoedown_autolink_flags; + + +/************* + * FUNCTIONS * + *************/ + +/* hoedown_autolink_is_safe: verify that a URL has a safe protocol */ +int hoedown_autolink_is_safe(const uint8_t *data, size_t size); + +/* hoedown_autolink__www: search for the next www link in data */ +size_t hoedown_autolink__www(size_t *rewind_p, hoedown_buffer *link, + uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags); + +/* hoedown_autolink__email: search for the next email in data */ +size_t hoedown_autolink__email(size_t *rewind_p, hoedown_buffer *link, + uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags); + +/* hoedown_autolink__url: search for the next URL in data */ +size_t hoedown_autolink__url(size_t *rewind_p, hoedown_buffer *link, + uint8_t *data, size_t offset, size_t size, hoedown_autolink_flags flags); + + +#ifdef __cplusplus +} +#endif + +#endif /** HOEDOWN_AUTOLINK_H **/ diff --git a/ultimmc/libraries/hoedown/include/hoedown/buffer.h b/ultimmc/libraries/hoedown/include/hoedown/buffer.h new file mode 100644 index 0000000..062d86c --- /dev/null +++ b/ultimmc/libraries/hoedown/include/hoedown/buffer.h @@ -0,0 +1,134 @@ +/* buffer.h - simple, fast buffers */ + +#ifndef HOEDOWN_BUFFER_H +#define HOEDOWN_BUFFER_H + +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(_MSC_VER) +#define __attribute__(x) +#define inline __inline +#define __builtin_expect(x,n) x +#endif + + +/********* + * TYPES * + *********/ + +typedef void *(*hoedown_realloc_callback)(void *, size_t); +typedef void (*hoedown_free_callback)(void *); + +struct hoedown_buffer { + uint8_t *data; /* actual character data */ + size_t size; /* size of the string */ + size_t asize; /* allocated size (0 = volatile buffer) */ + size_t unit; /* reallocation unit size (0 = read-only buffer) */ + + hoedown_realloc_callback data_realloc; + hoedown_free_callback data_free; + hoedown_free_callback buffer_free; +}; + +typedef struct hoedown_buffer hoedown_buffer; + + +/************* + * FUNCTIONS * + *************/ + +/* allocation wrappers */ +void *hoedown_malloc(size_t size) __attribute__ ((malloc)); +void *hoedown_calloc(size_t nmemb, size_t size) __attribute__ ((malloc)); +void *hoedown_realloc(void *ptr, size_t size) __attribute__ ((malloc)); + +/* hoedown_buffer_init: initialize a buffer with custom allocators */ +void hoedown_buffer_init( + hoedown_buffer *buffer, + size_t unit, + hoedown_realloc_callback data_realloc, + hoedown_free_callback data_free, + hoedown_free_callback buffer_free +); + +/* hoedown_buffer_uninit: uninitialize an existing buffer */ +void hoedown_buffer_uninit(hoedown_buffer *buf); + +/* hoedown_buffer_new: allocate a new buffer */ +hoedown_buffer *hoedown_buffer_new(size_t unit) __attribute__ ((malloc)); + +/* hoedown_buffer_reset: free internal data of the buffer */ +void hoedown_buffer_reset(hoedown_buffer *buf); + +/* hoedown_buffer_grow: increase the allocated size to the given value */ +void hoedown_buffer_grow(hoedown_buffer *buf, size_t neosz); + +/* hoedown_buffer_put: append raw data to a buffer */ +void hoedown_buffer_put(hoedown_buffer *buf, const uint8_t *data, size_t size); + +/* hoedown_buffer_puts: append a NUL-terminated string to a buffer */ +void hoedown_buffer_puts(hoedown_buffer *buf, const char *str); + +/* hoedown_buffer_putc: append a single char to a buffer */ +void hoedown_buffer_putc(hoedown_buffer *buf, uint8_t c); + +/* hoedown_buffer_putf: read from a file and append to a buffer, until EOF or error */ +int hoedown_buffer_putf(hoedown_buffer *buf, FILE* file); + +/* hoedown_buffer_set: replace the buffer's contents with raw data */ +void hoedown_buffer_set(hoedown_buffer *buf, const uint8_t *data, size_t size); + +/* hoedown_buffer_sets: replace the buffer's contents with a NUL-terminated string */ +void hoedown_buffer_sets(hoedown_buffer *buf, const char *str); + +/* hoedown_buffer_eq: compare a buffer's data with other data for equality */ +int hoedown_buffer_eq(const hoedown_buffer *buf, const uint8_t *data, size_t size); + +/* hoedown_buffer_eq: compare a buffer's data with NUL-terminated string for equality */ +int hoedown_buffer_eqs(const hoedown_buffer *buf, const char *str); + +/* hoedown_buffer_prefix: compare the beginning of a buffer with a string */ +int hoedown_buffer_prefix(const hoedown_buffer *buf, const char *prefix); + +/* hoedown_buffer_slurp: remove a given number of bytes from the head of the buffer */ +void hoedown_buffer_slurp(hoedown_buffer *buf, size_t size); + +/* hoedown_buffer_cstr: NUL-termination of the string array (making a C-string) */ +const char *hoedown_buffer_cstr(hoedown_buffer *buf); + +/* hoedown_buffer_printf: formatted printing to a buffer */ +void hoedown_buffer_printf(hoedown_buffer *buf, const char *fmt, ...) __attribute__ ((format (printf, 2, 3))); + +/* hoedown_buffer_put_utf8: put a Unicode character encoded as UTF-8 */ +void hoedown_buffer_put_utf8(hoedown_buffer *buf, unsigned int codepoint); + +/* hoedown_buffer_free: free the buffer */ +void hoedown_buffer_free(hoedown_buffer *buf); + + +/* HOEDOWN_BUFPUTSL: optimized hoedown_buffer_puts of a string literal */ +#define HOEDOWN_BUFPUTSL(output, literal) \ + hoedown_buffer_put(output, (const uint8_t *)literal, sizeof(literal) - 1) + +/* HOEDOWN_BUFSETSL: optimized hoedown_buffer_sets of a string literal */ +#define HOEDOWN_BUFSETSL(output, literal) \ + hoedown_buffer_set(output, (const uint8_t *)literal, sizeof(literal) - 1) + +/* HOEDOWN_BUFEQSL: optimized hoedown_buffer_eqs of a string literal */ +#define HOEDOWN_BUFEQSL(output, literal) \ + hoedown_buffer_eq(output, (const uint8_t *)literal, sizeof(literal) - 1) + + +#ifdef __cplusplus +} +#endif + +#endif /** HOEDOWN_BUFFER_H **/ diff --git a/ultimmc/libraries/hoedown/include/hoedown/document.h b/ultimmc/libraries/hoedown/include/hoedown/document.h new file mode 100644 index 0000000..210c565 --- /dev/null +++ b/ultimmc/libraries/hoedown/include/hoedown/document.h @@ -0,0 +1,172 @@ +/* document.h - generic markdown parser */ + +#ifndef HOEDOWN_DOCUMENT_H +#define HOEDOWN_DOCUMENT_H + +#include "buffer.h" +#include "autolink.h" + +#ifdef __cplusplus +extern "C" { +#endif + + +/************* + * CONSTANTS * + *************/ + +typedef enum hoedown_extensions { + /* block-level extensions */ + HOEDOWN_EXT_TABLES = (1 << 0), + HOEDOWN_EXT_FENCED_CODE = (1 << 1), + HOEDOWN_EXT_FOOTNOTES = (1 << 2), + + /* span-level extensions */ + HOEDOWN_EXT_AUTOLINK = (1 << 3), + HOEDOWN_EXT_STRIKETHROUGH = (1 << 4), + HOEDOWN_EXT_UNDERLINE = (1 << 5), + HOEDOWN_EXT_HIGHLIGHT = (1 << 6), + HOEDOWN_EXT_QUOTE = (1 << 7), + HOEDOWN_EXT_SUPERSCRIPT = (1 << 8), + HOEDOWN_EXT_MATH = (1 << 9), + + /* other flags */ + HOEDOWN_EXT_NO_INTRA_EMPHASIS = (1 << 11), + HOEDOWN_EXT_SPACE_HEADERS = (1 << 12), + HOEDOWN_EXT_MATH_EXPLICIT = (1 << 13), + + /* negative flags */ + HOEDOWN_EXT_DISABLE_INDENTED_CODE = (1 << 14) +} hoedown_extensions; + +#define HOEDOWN_EXT_BLOCK (\ + HOEDOWN_EXT_TABLES |\ + HOEDOWN_EXT_FENCED_CODE |\ + HOEDOWN_EXT_FOOTNOTES ) + +#define HOEDOWN_EXT_SPAN (\ + HOEDOWN_EXT_AUTOLINK |\ + HOEDOWN_EXT_STRIKETHROUGH |\ + HOEDOWN_EXT_UNDERLINE |\ + HOEDOWN_EXT_HIGHLIGHT |\ + HOEDOWN_EXT_QUOTE |\ + HOEDOWN_EXT_SUPERSCRIPT |\ + HOEDOWN_EXT_MATH ) + +#define HOEDOWN_EXT_FLAGS (\ + HOEDOWN_EXT_NO_INTRA_EMPHASIS |\ + HOEDOWN_EXT_SPACE_HEADERS |\ + HOEDOWN_EXT_MATH_EXPLICIT ) + +#define HOEDOWN_EXT_NEGATIVE (\ + HOEDOWN_EXT_DISABLE_INDENTED_CODE ) + +typedef enum hoedown_list_flags { + HOEDOWN_LIST_ORDERED = (1 << 0), + HOEDOWN_LI_BLOCK = (1 << 1) /*
  • containing block data */ +} hoedown_list_flags; + +typedef enum hoedown_table_flags { + HOEDOWN_TABLE_ALIGN_LEFT = 1, + HOEDOWN_TABLE_ALIGN_RIGHT = 2, + HOEDOWN_TABLE_ALIGN_CENTER = 3, + HOEDOWN_TABLE_ALIGNMASK = 3, + HOEDOWN_TABLE_HEADER = 4 +} hoedown_table_flags; + +typedef enum hoedown_autolink_type { + HOEDOWN_AUTOLINK_NONE, /* used internally when it is not an autolink*/ + HOEDOWN_AUTOLINK_NORMAL, /* normal http/http/ftp/mailto/etc link */ + HOEDOWN_AUTOLINK_EMAIL /* e-mail link without explit mailto: */ +} hoedown_autolink_type; + + +/********* + * TYPES * + *********/ + +struct hoedown_document; +typedef struct hoedown_document hoedown_document; + +struct hoedown_renderer_data { + void *opaque; +}; +typedef struct hoedown_renderer_data hoedown_renderer_data; + +/* hoedown_renderer - functions for rendering parsed data */ +struct hoedown_renderer { + /* state object */ + void *opaque; + + /* block level callbacks - NULL skips the block */ + void (*blockcode)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_buffer *lang, const hoedown_renderer_data *data); + void (*blockquote)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + void (*header)(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data); + void (*hrule)(hoedown_buffer *ob, const hoedown_renderer_data *data); + void (*list)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data); + void (*listitem)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data); + void (*paragraph)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + void (*table)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + void (*table_header)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + void (*table_body)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + void (*table_row)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + void (*table_cell)(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_table_flags flags, const hoedown_renderer_data *data); + void (*footnotes)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + void (*footnote_def)(hoedown_buffer *ob, const hoedown_buffer *content, unsigned int num, const hoedown_renderer_data *data); + void (*blockhtml)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); + + /* span level callbacks - NULL or return 0 prints the span verbatim */ + int (*autolink)(hoedown_buffer *ob, const hoedown_buffer *link, hoedown_autolink_type type, const hoedown_renderer_data *data); + int (*codespan)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); + int (*double_emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*underline)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*highlight)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*quote)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*image)(hoedown_buffer *ob, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_buffer *alt, const hoedown_renderer_data *data); + int (*linebreak)(hoedown_buffer *ob, const hoedown_renderer_data *data); + int (*link)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data); + int (*triple_emphasis)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*strikethrough)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*superscript)(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data); + int (*footnote_ref)(hoedown_buffer *ob, unsigned int num, const hoedown_renderer_data *data); + int (*math)(hoedown_buffer *ob, const hoedown_buffer *text, int displaymode, const hoedown_renderer_data *data); + int (*raw_html)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); + + /* low level callbacks - NULL copies input directly into the output */ + void (*entity)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); + void (*normal_text)(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data); + + /* miscellaneous callbacks */ + void (*doc_header)(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data); + void (*doc_footer)(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data); +}; +typedef struct hoedown_renderer hoedown_renderer; + + +/************* + * FUNCTIONS * + *************/ + +/* hoedown_document_new: allocate a new document processor instance */ +hoedown_document *hoedown_document_new( + const hoedown_renderer *renderer, + hoedown_extensions extensions, + size_t max_nesting +) __attribute__ ((malloc)); + +/* hoedown_document_render: render regular Markdown using the document processor */ +void hoedown_document_render(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size); + +/* hoedown_document_render_inline: render inline Markdown using the document processor */ +void hoedown_document_render_inline(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size); + +/* hoedown_document_free: deallocate a document processor instance */ +void hoedown_document_free(hoedown_document *doc); + + +#ifdef __cplusplus +} +#endif + +#endif /** HOEDOWN_DOCUMENT_H **/ diff --git a/ultimmc/libraries/hoedown/include/hoedown/escape.h b/ultimmc/libraries/hoedown/include/hoedown/escape.h new file mode 100644 index 0000000..d7659c2 --- /dev/null +++ b/ultimmc/libraries/hoedown/include/hoedown/escape.h @@ -0,0 +1,28 @@ +/* escape.h - escape utilities */ + +#ifndef HOEDOWN_ESCAPE_H +#define HOEDOWN_ESCAPE_H + +#include "buffer.h" + +#ifdef __cplusplus +extern "C" { +#endif + + +/************* + * FUNCTIONS * + *************/ + +/* hoedown_escape_href: escape (part of) a URL inside HTML */ +void hoedown_escape_href(hoedown_buffer *ob, const uint8_t *data, size_t size); + +/* hoedown_escape_html: escape HTML */ +void hoedown_escape_html(hoedown_buffer *ob, const uint8_t *data, size_t size, int secure); + + +#ifdef __cplusplus +} +#endif + +#endif /** HOEDOWN_ESCAPE_H **/ diff --git a/ultimmc/libraries/hoedown/include/hoedown/html.h b/ultimmc/libraries/hoedown/include/hoedown/html.h new file mode 100644 index 0000000..7c68809 --- /dev/null +++ b/ultimmc/libraries/hoedown/include/hoedown/html.h @@ -0,0 +1,84 @@ +/* html.h - HTML renderer and utilities */ + +#ifndef HOEDOWN_HTML_H +#define HOEDOWN_HTML_H + +#include "document.h" +#include "buffer.h" + +#ifdef __cplusplus +extern "C" { +#endif + + +/************* + * CONSTANTS * + *************/ + +typedef enum hoedown_html_flags { + HOEDOWN_HTML_SKIP_HTML = (1 << 0), + HOEDOWN_HTML_ESCAPE = (1 << 1), + HOEDOWN_HTML_HARD_WRAP = (1 << 2), + HOEDOWN_HTML_USE_XHTML = (1 << 3) +} hoedown_html_flags; + +typedef enum hoedown_html_tag { + HOEDOWN_HTML_TAG_NONE = 0, + HOEDOWN_HTML_TAG_OPEN, + HOEDOWN_HTML_TAG_CLOSE +} hoedown_html_tag; + + +/********* + * TYPES * + *********/ + +struct hoedown_html_renderer_state { + void *opaque; + + struct { + int header_count; + int current_level; + int level_offset; + int nesting_level; + } toc_data; + + hoedown_html_flags flags; + + /* extra callbacks */ + void (*link_attributes)(hoedown_buffer *ob, const hoedown_buffer *url, const hoedown_renderer_data *data); +}; +typedef struct hoedown_html_renderer_state hoedown_html_renderer_state; + + +/************* + * FUNCTIONS * + *************/ + +/* hoedown_html_smartypants: process an HTML snippet using SmartyPants for smart punctuation */ +void hoedown_html_smartypants(hoedown_buffer *ob, const uint8_t *data, size_t size); + +/* hoedown_html_is_tag: checks if data starts with a specific tag, returns the tag type or NONE */ +hoedown_html_tag hoedown_html_is_tag(const uint8_t *data, size_t size, const char *tagname); + + +/* hoedown_html_renderer_new: allocates a regular HTML renderer */ +hoedown_renderer *hoedown_html_renderer_new( + hoedown_html_flags render_flags, + int nesting_level +) __attribute__ ((malloc)); + +/* hoedown_html_toc_renderer_new: like hoedown_html_renderer_new, but the returned renderer produces the Table of Contents */ +hoedown_renderer *hoedown_html_toc_renderer_new( + int nesting_level +) __attribute__ ((malloc)); + +/* hoedown_html_renderer_free: deallocate an HTML renderer */ +void hoedown_html_renderer_free(hoedown_renderer *renderer); + + +#ifdef __cplusplus +} +#endif + +#endif /** HOEDOWN_HTML_H **/ diff --git a/ultimmc/libraries/hoedown/include/hoedown/stack.h b/ultimmc/libraries/hoedown/include/hoedown/stack.h new file mode 100644 index 0000000..d1855f4 --- /dev/null +++ b/ultimmc/libraries/hoedown/include/hoedown/stack.h @@ -0,0 +1,52 @@ +/* stack.h - simple stacking */ + +#ifndef HOEDOWN_STACK_H +#define HOEDOWN_STACK_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + + +/********* + * TYPES * + *********/ + +struct hoedown_stack { + void **item; + size_t size; + size_t asize; +}; +typedef struct hoedown_stack hoedown_stack; + + +/************* + * FUNCTIONS * + *************/ + +/* hoedown_stack_init: initialize a stack */ +void hoedown_stack_init(hoedown_stack *st, size_t initial_size); + +/* hoedown_stack_uninit: free internal data of the stack */ +void hoedown_stack_uninit(hoedown_stack *st); + +/* hoedown_stack_grow: increase the allocated size to the given value */ +void hoedown_stack_grow(hoedown_stack *st, size_t neosz); + +/* hoedown_stack_push: push an item to the top of the stack */ +void hoedown_stack_push(hoedown_stack *st, void *item); + +/* hoedown_stack_pop: retrieve and remove the item at the top of the stack */ +void *hoedown_stack_pop(hoedown_stack *st); + +/* hoedown_stack_top: retrieve the item at the top of the stack */ +void *hoedown_stack_top(const hoedown_stack *st); + + +#ifdef __cplusplus +} +#endif + +#endif /** HOEDOWN_STACK_H **/ diff --git a/ultimmc/libraries/hoedown/include/hoedown/version.h b/ultimmc/libraries/hoedown/include/hoedown/version.h new file mode 100644 index 0000000..4938cae --- /dev/null +++ b/ultimmc/libraries/hoedown/include/hoedown/version.h @@ -0,0 +1,33 @@ +/* version.h - holds Hoedown's version */ + +#ifndef HOEDOWN_VERSION_H +#define HOEDOWN_VERSION_H + +#ifdef __cplusplus +extern "C" { +#endif + + +/************* + * CONSTANTS * + *************/ + +#define HOEDOWN_VERSION "3.0.2" +#define HOEDOWN_VERSION_MAJOR 3 +#define HOEDOWN_VERSION_MINOR 0 +#define HOEDOWN_VERSION_REVISION 2 + + +/************* + * FUNCTIONS * + *************/ + +/* hoedown_version: retrieve Hoedown's version numbers */ +void hoedown_version(int *major, int *minor, int *revision); + + +#ifdef __cplusplus +} +#endif + +#endif /** HOEDOWN_VERSION_H **/ diff --git a/ultimmc/libraries/hoedown/src/autolink.c b/ultimmc/libraries/hoedown/src/autolink.c new file mode 100644 index 0000000..3063b1a --- /dev/null +++ b/ultimmc/libraries/hoedown/src/autolink.c @@ -0,0 +1,281 @@ +#include "hoedown/autolink.h" + +#include +#include +#include +#include + +#ifndef _MSC_VER +#include +#else +#define strncasecmp _strnicmp +#endif + +int +hoedown_autolink_is_safe(const uint8_t *data, size_t size) +{ + static const size_t valid_uris_count = 6; + static const char *valid_uris[] = { + "http://", "https://", "/", "#", "ftp://", "mailto:" + }; + static const size_t valid_uris_size[] = { 7, 8, 1, 1, 6, 7 }; + size_t i; + + for (i = 0; i < valid_uris_count; ++i) { + size_t len = valid_uris_size[i]; + + if (size > len && + strncasecmp((char *)data, valid_uris[i], len) == 0 && + isalnum(data[len])) + return 1; + } + + return 0; +} + +static size_t +autolink_delim(uint8_t *data, size_t link_end, size_t max_rewind, size_t size) +{ + uint8_t cclose, copen = 0; + size_t i; + + for (i = 0; i < link_end; ++i) + if (data[i] == '<') { + link_end = i; + break; + } + + while (link_end > 0) { + if (strchr("?!.,:", data[link_end - 1]) != NULL) + link_end--; + + else if (data[link_end - 1] == ';') { + size_t new_end = link_end - 2; + + while (new_end > 0 && isalpha(data[new_end])) + new_end--; + + if (new_end < link_end - 2 && data[new_end] == '&') + link_end = new_end; + else + link_end--; + } + else break; + } + + if (link_end == 0) + return 0; + + cclose = data[link_end - 1]; + + switch (cclose) { + case '"': copen = '"'; break; + case '\'': copen = '\''; break; + case ')': copen = '('; break; + case ']': copen = '['; break; + case '}': copen = '{'; break; + } + + if (copen != 0) { + size_t closing = 0; + size_t opening = 0; + size_t i = 0; + + /* Try to close the final punctuation sign in this same line; + * if we managed to close it outside of the URL, that means that it's + * not part of the URL. If it closes inside the URL, that means it + * is part of the URL. + * + * Examples: + * + * foo http://www.pokemon.com/Pikachu_(Electric) bar + * => http://www.pokemon.com/Pikachu_(Electric) + * + * foo (http://www.pokemon.com/Pikachu_(Electric)) bar + * => http://www.pokemon.com/Pikachu_(Electric) + * + * foo http://www.pokemon.com/Pikachu_(Electric)) bar + * => http://www.pokemon.com/Pikachu_(Electric)) + * + * (foo http://www.pokemon.com/Pikachu_(Electric)) bar + * => foo http://www.pokemon.com/Pikachu_(Electric) + */ + + while (i < link_end) { + if (data[i] == copen) + opening++; + else if (data[i] == cclose) + closing++; + + i++; + } + + if (closing != opening) + link_end--; + } + + return link_end; +} + +static size_t +check_domain(uint8_t *data, size_t size, int allow_short) +{ + size_t i, np = 0; + + if (!isalnum(data[0])) + return 0; + + for (i = 1; i < size - 1; ++i) { + if (strchr(".:", data[i]) != NULL) np++; + else if (!isalnum(data[i]) && data[i] != '-') break; + } + + if (allow_short) { + /* We don't need a valid domain in the strict sense (with + * least one dot; so just make sure it's composed of valid + * domain characters and return the length of the the valid + * sequence. */ + return i; + } else { + /* a valid domain needs to have at least a dot. + * that's as far as we get */ + return np ? i : 0; + } +} + +size_t +hoedown_autolink__www( + size_t *rewind_p, + hoedown_buffer *link, + uint8_t *data, + size_t max_rewind, + size_t size, + unsigned int flags) +{ + size_t link_end; + + if (max_rewind > 0 && !ispunct(data[-1]) && !isspace(data[-1])) + return 0; + + if (size < 4 || memcmp(data, "www.", strlen("www.")) != 0) + return 0; + + link_end = check_domain(data, size, 0); + + if (link_end == 0) + return 0; + + while (link_end < size && !isspace(data[link_end])) + link_end++; + + link_end = autolink_delim(data, link_end, max_rewind, size); + + if (link_end == 0) + return 0; + + hoedown_buffer_put(link, data, link_end); + *rewind_p = 0; + + return (int)link_end; +} + +size_t +hoedown_autolink__email( + size_t *rewind_p, + hoedown_buffer *link, + uint8_t *data, + size_t max_rewind, + size_t size, + unsigned int flags) +{ + size_t link_end, rewind; + int nb = 0, np = 0; + + for (rewind = 0; rewind < max_rewind; ++rewind) { + uint8_t c = data[-1 - rewind]; + + if (isalnum(c)) + continue; + + if (strchr(".+-_", c) != NULL) + continue; + + break; + } + + if (rewind == 0) + return 0; + + for (link_end = 0; link_end < size; ++link_end) { + uint8_t c = data[link_end]; + + if (isalnum(c)) + continue; + + if (c == '@') + nb++; + else if (c == '.' && link_end < size - 1) + np++; + else if (c != '-' && c != '_') + break; + } + + if (link_end < 2 || nb != 1 || np == 0 || + !isalpha(data[link_end - 1])) + return 0; + + link_end = autolink_delim(data, link_end, max_rewind, size); + + if (link_end == 0) + return 0; + + hoedown_buffer_put(link, data - rewind, link_end + rewind); + *rewind_p = rewind; + + return link_end; +} + +size_t +hoedown_autolink__url( + size_t *rewind_p, + hoedown_buffer *link, + uint8_t *data, + size_t max_rewind, + size_t size, + unsigned int flags) +{ + size_t link_end, rewind = 0, domain_len; + + if (size < 4 || data[1] != '/' || data[2] != '/') + return 0; + + while (rewind < max_rewind && isalpha(data[-1 - rewind])) + rewind++; + + if (!hoedown_autolink_is_safe(data - rewind, size + rewind)) + return 0; + + link_end = strlen("://"); + + domain_len = check_domain( + data + link_end, + size - link_end, + flags & HOEDOWN_AUTOLINK_SHORT_DOMAINS); + + if (domain_len == 0) + return 0; + + link_end += domain_len; + while (link_end < size && !isspace(data[link_end])) + link_end++; + + link_end = autolink_delim(data, link_end, max_rewind, size); + + if (link_end == 0) + return 0; + + hoedown_buffer_put(link, data - rewind, link_end + rewind); + *rewind_p = rewind; + + return link_end; +} diff --git a/ultimmc/libraries/hoedown/src/buffer.c b/ultimmc/libraries/hoedown/src/buffer.c new file mode 100644 index 0000000..024a8bc --- /dev/null +++ b/ultimmc/libraries/hoedown/src/buffer.c @@ -0,0 +1,308 @@ +#include "hoedown/buffer.h" + +#include +#include +#include +#include + +void * +hoedown_malloc(size_t size) +{ + void *ret = malloc(size); + + if (!ret) { + fprintf(stderr, "Allocation failed.\n"); + abort(); + } + + return ret; +} + +void * +hoedown_calloc(size_t nmemb, size_t size) +{ + void *ret = calloc(nmemb, size); + + if (!ret) { + fprintf(stderr, "Allocation failed.\n"); + abort(); + } + + return ret; +} + +void * +hoedown_realloc(void *ptr, size_t size) +{ + void *ret = realloc(ptr, size); + + if (!ret) { + fprintf(stderr, "Allocation failed.\n"); + abort(); + } + + return ret; +} + +void +hoedown_buffer_init( + hoedown_buffer *buf, + size_t unit, + hoedown_realloc_callback data_realloc, + hoedown_free_callback data_free, + hoedown_free_callback buffer_free) +{ + assert(buf); + + buf->data = NULL; + buf->size = buf->asize = 0; + buf->unit = unit; + buf->data_realloc = data_realloc; + buf->data_free = data_free; + buf->buffer_free = buffer_free; +} + +void +hoedown_buffer_uninit(hoedown_buffer *buf) +{ + assert(buf && buf->unit); + buf->data_free(buf->data); +} + +hoedown_buffer * +hoedown_buffer_new(size_t unit) +{ + hoedown_buffer *ret = hoedown_malloc(sizeof (hoedown_buffer)); + hoedown_buffer_init(ret, unit, hoedown_realloc, free, free); + return ret; +} + +void +hoedown_buffer_free(hoedown_buffer *buf) +{ + if (!buf) return; + assert(buf && buf->unit); + + buf->data_free(buf->data); + + if (buf->buffer_free) + buf->buffer_free(buf); +} + +void +hoedown_buffer_reset(hoedown_buffer *buf) +{ + assert(buf && buf->unit); + + buf->data_free(buf->data); + buf->data = NULL; + buf->size = buf->asize = 0; +} + +void +hoedown_buffer_grow(hoedown_buffer *buf, size_t neosz) +{ + size_t neoasz; + assert(buf && buf->unit); + + if (buf->asize >= neosz) + return; + + neoasz = buf->asize + buf->unit; + while (neoasz < neosz) + neoasz += buf->unit; + + buf->data = (uint8_t *) buf->data_realloc(buf->data, neoasz); + buf->asize = neoasz; +} + +void +hoedown_buffer_put(hoedown_buffer *buf, const uint8_t *data, size_t size) +{ + assert(buf && buf->unit); + + if (buf->size + size > buf->asize) + hoedown_buffer_grow(buf, buf->size + size); + + memcpy(buf->data + buf->size, data, size); + buf->size += size; +} + +void +hoedown_buffer_puts(hoedown_buffer *buf, const char *str) +{ + hoedown_buffer_put(buf, (const uint8_t *)str, strlen(str)); +} + +void +hoedown_buffer_putc(hoedown_buffer *buf, uint8_t c) +{ + assert(buf && buf->unit); + + if (buf->size >= buf->asize) + hoedown_buffer_grow(buf, buf->size + 1); + + buf->data[buf->size] = c; + buf->size += 1; +} + +int +hoedown_buffer_putf(hoedown_buffer *buf, FILE *file) +{ + assert(buf && buf->unit); + + while (!(feof(file) || ferror(file))) { + hoedown_buffer_grow(buf, buf->size + buf->unit); + buf->size += fread(buf->data + buf->size, 1, buf->unit, file); + } + + return ferror(file); +} + +void +hoedown_buffer_set(hoedown_buffer *buf, const uint8_t *data, size_t size) +{ + assert(buf && buf->unit); + + if (size > buf->asize) + hoedown_buffer_grow(buf, size); + + memcpy(buf->data, data, size); + buf->size = size; +} + +void +hoedown_buffer_sets(hoedown_buffer *buf, const char *str) +{ + hoedown_buffer_set(buf, (const uint8_t *)str, strlen(str)); +} + +int +hoedown_buffer_eq(const hoedown_buffer *buf, const uint8_t *data, size_t size) +{ + if (buf->size != size) return 0; + return memcmp(buf->data, data, size) == 0; +} + +int +hoedown_buffer_eqs(const hoedown_buffer *buf, const char *str) +{ + return hoedown_buffer_eq(buf, (const uint8_t *)str, strlen(str)); +} + +int +hoedown_buffer_prefix(const hoedown_buffer *buf, const char *prefix) +{ + size_t i; + + for (i = 0; i < buf->size; ++i) { + if (prefix[i] == 0) + return 0; + + if (buf->data[i] != prefix[i]) + return buf->data[i] - prefix[i]; + } + + return 0; +} + +void +hoedown_buffer_slurp(hoedown_buffer *buf, size_t size) +{ + assert(buf && buf->unit); + + if (size >= buf->size) { + buf->size = 0; + return; + } + + buf->size -= size; + memmove(buf->data, buf->data + size, buf->size); +} + +const char * +hoedown_buffer_cstr(hoedown_buffer *buf) +{ + assert(buf && buf->unit); + + if (buf->size < buf->asize && buf->data[buf->size] == 0) + return (char *)buf->data; + + hoedown_buffer_grow(buf, buf->size + 1); + buf->data[buf->size] = 0; + + return (char *)buf->data; +} + +void +hoedown_buffer_printf(hoedown_buffer *buf, const char *fmt, ...) +{ + va_list ap; + int n; + + assert(buf && buf->unit); + + if (buf->size >= buf->asize) + hoedown_buffer_grow(buf, buf->size + 1); + + va_start(ap, fmt); + n = vsnprintf((char *)buf->data + buf->size, buf->asize - buf->size, fmt, ap); + va_end(ap); + + if (n < 0) { +#ifndef _MSC_VER + return; +#else + va_start(ap, fmt); + n = _vscprintf(fmt, ap); + va_end(ap); +#endif + } + + if ((size_t)n >= buf->asize - buf->size) { + hoedown_buffer_grow(buf, buf->size + n + 1); + + va_start(ap, fmt); + n = vsnprintf((char *)buf->data + buf->size, buf->asize - buf->size, fmt, ap); + va_end(ap); + } + + if (n < 0) + return; + + buf->size += n; +} + +void hoedown_buffer_put_utf8(hoedown_buffer *buf, unsigned int c) { + unsigned char unichar[4]; + + assert(buf && buf->unit); + + if (c < 0x80) { + hoedown_buffer_putc(buf, c); + } + else if (c < 0x800) { + unichar[0] = 192 + (c / 64); + unichar[1] = 128 + (c % 64); + hoedown_buffer_put(buf, unichar, 2); + } + else if (c - 0xd800u < 0x800) { + HOEDOWN_BUFPUTSL(buf, "\xef\xbf\xbd"); + } + else if (c < 0x10000) { + unichar[0] = 224 + (c / 4096); + unichar[1] = 128 + (c / 64) % 64; + unichar[2] = 128 + (c % 64); + hoedown_buffer_put(buf, unichar, 3); + } + else if (c < 0x110000) { + unichar[0] = 240 + (c / 262144); + unichar[1] = 128 + (c / 4096) % 64; + unichar[2] = 128 + (c / 64) % 64; + unichar[3] = 128 + (c % 64); + hoedown_buffer_put(buf, unichar, 4); + } + else { + HOEDOWN_BUFPUTSL(buf, "\xef\xbf\xbd"); + } +} diff --git a/ultimmc/libraries/hoedown/src/document.c b/ultimmc/libraries/hoedown/src/document.c new file mode 100644 index 0000000..e9e2ab1 --- /dev/null +++ b/ultimmc/libraries/hoedown/src/document.c @@ -0,0 +1,2958 @@ +#include "hoedown/document.h" + +#include +#include +#include +#include + +#include "hoedown/stack.h" + +#ifndef _MSC_VER +#include +#else +#define strncasecmp _strnicmp +#endif + +#define REF_TABLE_SIZE 8 + +#define BUFFER_BLOCK 0 +#define BUFFER_SPAN 1 + +#define HOEDOWN_LI_END 8 /* internal list flag */ + +const char *hoedown_find_block_tag(const char *str, unsigned int len); + +/*************** + * LOCAL TYPES * + ***************/ + +/* link_ref: reference to a link */ +struct link_ref { + unsigned int id; + + hoedown_buffer *link; + hoedown_buffer *title; + + struct link_ref *next; +}; + +/* footnote_ref: reference to a footnote */ +struct footnote_ref { + unsigned int id; + + int is_used; + unsigned int num; + + hoedown_buffer *contents; +}; + +/* footnote_item: an item in a footnote_list */ +struct footnote_item { + struct footnote_ref *ref; + struct footnote_item *next; +}; + +/* footnote_list: linked list of footnote_item */ +struct footnote_list { + unsigned int count; + struct footnote_item *head; + struct footnote_item *tail; +}; + +/* char_trigger: function pointer to render active chars */ +/* returns the number of chars taken care of */ +/* data is the pointer of the beginning of the span */ +/* offset is the number of valid chars before data */ +typedef size_t +(*char_trigger)(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); + +static size_t char_emphasis(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_quote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_linebreak(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_codespan(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_escape(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_entity(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_langle_tag(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_autolink_url(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_autolink_email(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_autolink_www(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_link(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_superscript(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); +static size_t char_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size); + +enum markdown_char_t { + MD_CHAR_NONE = 0, + MD_CHAR_EMPHASIS, + MD_CHAR_CODESPAN, + MD_CHAR_LINEBREAK, + MD_CHAR_LINK, + MD_CHAR_LANGLE, + MD_CHAR_ESCAPE, + MD_CHAR_ENTITY, + MD_CHAR_AUTOLINK_URL, + MD_CHAR_AUTOLINK_EMAIL, + MD_CHAR_AUTOLINK_WWW, + MD_CHAR_SUPERSCRIPT, + MD_CHAR_QUOTE, + MD_CHAR_MATH +}; + +static char_trigger markdown_char_ptrs[] = { + NULL, + &char_emphasis, + &char_codespan, + &char_linebreak, + &char_link, + &char_langle_tag, + &char_escape, + &char_entity, + &char_autolink_url, + &char_autolink_email, + &char_autolink_www, + &char_superscript, + &char_quote, + &char_math +}; + +struct hoedown_document { + hoedown_renderer md; + hoedown_renderer_data data; + + struct link_ref *refs[REF_TABLE_SIZE]; + struct footnote_list footnotes_found; + struct footnote_list footnotes_used; + uint8_t active_char[256]; + hoedown_stack work_bufs[2]; + hoedown_extensions ext_flags; + size_t max_nesting; + int in_link_body; +}; + +/*************************** + * HELPER FUNCTIONS * + ***************************/ + +static hoedown_buffer * +newbuf(hoedown_document *doc, int type) +{ + static const size_t buf_size[2] = {256, 64}; + hoedown_buffer *work = NULL; + hoedown_stack *pool = &doc->work_bufs[type]; + + if (pool->size < pool->asize && + pool->item[pool->size] != NULL) { + work = pool->item[pool->size++]; + work->size = 0; + } else { + work = hoedown_buffer_new(buf_size[type]); + hoedown_stack_push(pool, work); + } + + return work; +} + +static void +popbuf(hoedown_document *doc, int type) +{ + doc->work_bufs[type].size--; +} + +static void +unscape_text(hoedown_buffer *ob, hoedown_buffer *src) +{ + size_t i = 0, org; + while (i < src->size) { + org = i; + while (i < src->size && src->data[i] != '\\') + i++; + + if (i > org) + hoedown_buffer_put(ob, src->data + org, i - org); + + if (i + 1 >= src->size) + break; + + hoedown_buffer_putc(ob, src->data[i + 1]); + i += 2; + } +} + +static unsigned int +hash_link_ref(const uint8_t *link_ref, size_t length) +{ + size_t i; + unsigned int hash = 0; + + for (i = 0; i < length; ++i) + hash = tolower(link_ref[i]) + (hash << 6) + (hash << 16) - hash; + + return hash; +} + +static struct link_ref * +add_link_ref( + struct link_ref **references, + const uint8_t *name, size_t name_size) +{ + struct link_ref *ref = hoedown_calloc(1, sizeof(struct link_ref)); + + ref->id = hash_link_ref(name, name_size); + ref->next = references[ref->id % REF_TABLE_SIZE]; + + references[ref->id % REF_TABLE_SIZE] = ref; + return ref; +} + +static struct link_ref * +find_link_ref(struct link_ref **references, uint8_t *name, size_t length) +{ + unsigned int hash = hash_link_ref(name, length); + struct link_ref *ref = NULL; + + ref = references[hash % REF_TABLE_SIZE]; + + while (ref != NULL) { + if (ref->id == hash) + return ref; + + ref = ref->next; + } + + return NULL; +} + +static void +free_link_refs(struct link_ref **references) +{ + size_t i; + + for (i = 0; i < REF_TABLE_SIZE; ++i) { + struct link_ref *r = references[i]; + struct link_ref *next; + + while (r) { + next = r->next; + hoedown_buffer_free(r->link); + hoedown_buffer_free(r->title); + free(r); + r = next; + } + } +} + +static struct footnote_ref * +create_footnote_ref(struct footnote_list *list, const uint8_t *name, size_t name_size) +{ + struct footnote_ref *ref = hoedown_calloc(1, sizeof(struct footnote_ref)); + + ref->id = hash_link_ref(name, name_size); + + return ref; +} + +static int +add_footnote_ref(struct footnote_list *list, struct footnote_ref *ref) +{ + struct footnote_item *item = hoedown_calloc(1, sizeof(struct footnote_item)); + if (!item) + return 0; + item->ref = ref; + + if (list->head == NULL) { + list->head = list->tail = item; + } else { + list->tail->next = item; + list->tail = item; + } + list->count++; + + return 1; +} + +static struct footnote_ref * +find_footnote_ref(struct footnote_list *list, uint8_t *name, size_t length) +{ + unsigned int hash = hash_link_ref(name, length); + struct footnote_item *item = NULL; + + item = list->head; + + while (item != NULL) { + if (item->ref->id == hash) + return item->ref; + item = item->next; + } + + return NULL; +} + +static void +free_footnote_ref(struct footnote_ref *ref) +{ + hoedown_buffer_free(ref->contents); + free(ref); +} + +static void +free_footnote_list(struct footnote_list *list, int free_refs) +{ + struct footnote_item *item = list->head; + struct footnote_item *next; + + while (item) { + next = item->next; + if (free_refs) + free_footnote_ref(item->ref); + free(item); + item = next; + } +} + + +/* + * Check whether a char is a Markdown spacing char. + + * Right now we only consider spaces the actual + * space and a newline: tabs and carriage returns + * are filtered out during the preprocessing phase. + * + * If we wanted to actually be UTF-8 compliant, we + * should instead extract an Unicode codepoint from + * this character and check for space properties. + */ +static int +_isspace(int c) +{ + return c == ' ' || c == '\n'; +} + +/* is_empty_all: verify that all the data is spacing */ +static int +is_empty_all(const uint8_t *data, size_t size) +{ + size_t i = 0; + while (i < size && _isspace(data[i])) i++; + return i == size; +} + +/* + * Replace all spacing characters in data with spaces. As a special + * case, this collapses a newline with the previous space, if possible. + */ +static void +replace_spacing(hoedown_buffer *ob, const uint8_t *data, size_t size) +{ + size_t i = 0, mark; + hoedown_buffer_grow(ob, size); + while (1) { + mark = i; + while (i < size && data[i] != '\n') i++; + hoedown_buffer_put(ob, data + mark, i - mark); + + if (i >= size) break; + + if (!(i > 0 && data[i-1] == ' ')) + hoedown_buffer_putc(ob, ' '); + i++; + } +} + +/**************************** + * INLINE PARSING FUNCTIONS * + ****************************/ + +/* is_mail_autolink • looks for the address part of a mail autolink and '>' */ +/* this is less strict than the original markdown e-mail address matching */ +static size_t +is_mail_autolink(uint8_t *data, size_t size) +{ + size_t i = 0, nb = 0; + + /* address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' */ + for (i = 0; i < size; ++i) { + if (isalnum(data[i])) + continue; + + switch (data[i]) { + case '@': + nb++; + + case '-': + case '.': + case '_': + break; + + case '>': + return (nb == 1) ? i + 1 : 0; + + default: + return 0; + } + } + + return 0; +} + +/* tag_length • returns the length of the given tag, or 0 is it's not valid */ +static size_t +tag_length(uint8_t *data, size_t size, hoedown_autolink_type *autolink) +{ + size_t i, j; + + /* a valid tag can't be shorter than 3 chars */ + if (size < 3) return 0; + + /* begins with a '<' optionally followed by '/', followed by letter or number */ + if (data[0] != '<') return 0; + i = (data[1] == '/') ? 2 : 1; + + if (!isalnum(data[i])) + return 0; + + /* scheme test */ + *autolink = HOEDOWN_AUTOLINK_NONE; + + /* try to find the beginning of an URI */ + while (i < size && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-')) + i++; + + if (i > 1 && data[i] == '@') { + if ((j = is_mail_autolink(data + i, size - i)) != 0) { + *autolink = HOEDOWN_AUTOLINK_EMAIL; + return i + j; + } + } + + if (i > 2 && data[i] == ':') { + *autolink = HOEDOWN_AUTOLINK_NORMAL; + i++; + } + + /* completing autolink test: no spacing or ' or " */ + if (i >= size) + *autolink = HOEDOWN_AUTOLINK_NONE; + + else if (*autolink) { + j = i; + + while (i < size) { + if (data[i] == '\\') i += 2; + else if (data[i] == '>' || data[i] == '\'' || + data[i] == '"' || data[i] == ' ' || data[i] == '\n') + break; + else i++; + } + + if (i >= size) return 0; + if (i > j && data[i] == '>') return i + 1; + /* one of the forbidden chars has been found */ + *autolink = HOEDOWN_AUTOLINK_NONE; + } + + /* looking for something looking like a tag end */ + while (i < size && data[i] != '>') i++; + if (i >= size) return 0; + return i + 1; +} + +/* parse_inline • parses inline markdown elements */ +static void +parse_inline(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) +{ + size_t i = 0, end = 0, consumed = 0; + hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL }; + uint8_t *active_char = doc->active_char; + + if (doc->work_bufs[BUFFER_SPAN].size + + doc->work_bufs[BUFFER_BLOCK].size > doc->max_nesting) + return; + + while (i < size) { + /* copying inactive chars into the output */ + while (end < size && active_char[data[end]] == 0) + end++; + + if (doc->md.normal_text) { + work.data = data + i; + work.size = end - i; + doc->md.normal_text(ob, &work, &doc->data); + } + else + hoedown_buffer_put(ob, data + i, end - i); + + if (end >= size) break; + i = end; + + end = markdown_char_ptrs[ (int)active_char[data[end]] ](ob, doc, data + i, i - consumed, size - i); + if (!end) /* no action from the callback */ + end = i + 1; + else { + i += end; + end = i; + consumed = i; + } + } +} + +/* is_escaped • returns whether special char at data[loc] is escaped by '\\' */ +static int +is_escaped(uint8_t *data, size_t loc) +{ + size_t i = loc; + while (i >= 1 && data[i - 1] == '\\') + i--; + + /* odd numbers of backslashes escapes data[loc] */ + return (loc - i) % 2; +} + +/* find_emph_char • looks for the next emph uint8_t, skipping other constructs */ +static size_t +find_emph_char(uint8_t *data, size_t size, uint8_t c) +{ + size_t i = 0; + + while (i < size) { + while (i < size && data[i] != c && data[i] != '[' && data[i] != '`') + i++; + + if (i == size) + return 0; + + /* not counting escaped chars */ + if (is_escaped(data, i)) { + i++; continue; + } + + if (data[i] == c) + return i; + + /* skipping a codespan */ + if (data[i] == '`') { + size_t span_nb = 0, bt; + size_t tmp_i = 0; + + /* counting the number of opening backticks */ + while (i < size && data[i] == '`') { + i++; span_nb++; + } + + if (i >= size) return 0; + + /* finding the matching closing sequence */ + bt = 0; + while (i < size && bt < span_nb) { + if (!tmp_i && data[i] == c) tmp_i = i; + if (data[i] == '`') bt++; + else bt = 0; + i++; + } + + /* not a well-formed codespan; use found matching emph char */ + if (i >= size) return tmp_i; + } + /* skipping a link */ + else if (data[i] == '[') { + size_t tmp_i = 0; + uint8_t cc; + + i++; + while (i < size && data[i] != ']') { + if (!tmp_i && data[i] == c) tmp_i = i; + i++; + } + + i++; + while (i < size && _isspace(data[i])) + i++; + + if (i >= size) + return tmp_i; + + switch (data[i]) { + case '[': + cc = ']'; break; + + case '(': + cc = ')'; break; + + default: + if (tmp_i) + return tmp_i; + else + continue; + } + + i++; + while (i < size && data[i] != cc) { + if (!tmp_i && data[i] == c) tmp_i = i; + i++; + } + + if (i >= size) + return tmp_i; + + i++; + } + } + + return 0; +} + +/* parse_emph1 • parsing single emphase */ +/* closed by a symbol not preceded by spacing and not followed by symbol */ +static size_t +parse_emph1(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c) +{ + size_t i = 0, len; + hoedown_buffer *work = 0; + int r; + + /* skipping one symbol if coming from emph3 */ + if (size > 1 && data[0] == c && data[1] == c) i = 1; + + while (i < size) { + len = find_emph_char(data + i, size - i, c); + if (!len) return 0; + i += len; + if (i >= size) return 0; + + if (data[i] == c && !_isspace(data[i - 1])) { + + if (doc->ext_flags & HOEDOWN_EXT_NO_INTRA_EMPHASIS) { + if (i + 1 < size && isalnum(data[i + 1])) + continue; + } + + work = newbuf(doc, BUFFER_SPAN); + parse_inline(work, doc, data, i); + + if (doc->ext_flags & HOEDOWN_EXT_UNDERLINE && c == '_') + r = doc->md.underline(ob, work, &doc->data); + else + r = doc->md.emphasis(ob, work, &doc->data); + + popbuf(doc, BUFFER_SPAN); + return r ? i + 1 : 0; + } + } + + return 0; +} + +/* parse_emph2 • parsing single emphase */ +static size_t +parse_emph2(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c) +{ + size_t i = 0, len; + hoedown_buffer *work = 0; + int r; + + while (i < size) { + len = find_emph_char(data + i, size - i, c); + if (!len) return 0; + i += len; + + if (i + 1 < size && data[i] == c && data[i + 1] == c && i && !_isspace(data[i - 1])) { + work = newbuf(doc, BUFFER_SPAN); + parse_inline(work, doc, data, i); + + if (c == '~') + r = doc->md.strikethrough(ob, work, &doc->data); + else if (c == '=') + r = doc->md.highlight(ob, work, &doc->data); + else + r = doc->md.double_emphasis(ob, work, &doc->data); + + popbuf(doc, BUFFER_SPAN); + return r ? i + 2 : 0; + } + i++; + } + return 0; +} + +/* parse_emph3 • parsing single emphase */ +/* finds the first closing tag, and delegates to the other emph */ +static size_t +parse_emph3(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, uint8_t c) +{ + size_t i = 0, len; + int r; + + while (i < size) { + len = find_emph_char(data + i, size - i, c); + if (!len) return 0; + i += len; + + /* skip spacing preceded symbols */ + if (data[i] != c || _isspace(data[i - 1])) + continue; + + if (i + 2 < size && data[i + 1] == c && data[i + 2] == c && doc->md.triple_emphasis) { + /* triple symbol found */ + hoedown_buffer *work = newbuf(doc, BUFFER_SPAN); + + parse_inline(work, doc, data, i); + r = doc->md.triple_emphasis(ob, work, &doc->data); + popbuf(doc, BUFFER_SPAN); + return r ? i + 3 : 0; + + } else if (i + 1 < size && data[i + 1] == c) { + /* double symbol found, handing over to emph1 */ + len = parse_emph1(ob, doc, data - 2, size + 2, c); + if (!len) return 0; + else return len - 2; + + } else { + /* single symbol found, handing over to emph2 */ + len = parse_emph2(ob, doc, data - 1, size + 1, c); + if (!len) return 0; + else return len - 1; + } + } + return 0; +} + +/* parse_math • parses a math span until the given ending delimiter */ +static size_t +parse_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size, const char *end, size_t delimsz, int displaymode) +{ + hoedown_buffer text = { NULL, 0, 0, 0, NULL, NULL, NULL }; + size_t i = delimsz; + + if (!doc->md.math) + return 0; + + /* find ending delimiter */ + while (1) { + while (i < size && data[i] != (uint8_t)end[0]) + i++; + + if (i >= size) + return 0; + + if (!is_escaped(data, i) && !(i + delimsz > size) + && memcmp(data + i, end, delimsz) == 0) + break; + + i++; + } + + /* prepare buffers */ + text.data = data + delimsz; + text.size = i - delimsz; + + /* if this is a $$ and MATH_EXPLICIT is not active, + * guess whether displaymode should be enabled from the context */ + i += delimsz; + if (delimsz == 2 && !(doc->ext_flags & HOEDOWN_EXT_MATH_EXPLICIT)) + displaymode = is_empty_all(data - offset, offset) && is_empty_all(data + i, size - i); + + /* call callback */ + if (doc->md.math(ob, &text, displaymode, &doc->data)) + return i; + + return 0; +} + +/* char_emphasis • single and double emphasis parsing */ +static size_t +char_emphasis(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + uint8_t c = data[0]; + size_t ret; + + if (doc->ext_flags & HOEDOWN_EXT_NO_INTRA_EMPHASIS) { + if (offset > 0 && !_isspace(data[-1]) && data[-1] != '>' && data[-1] != '(') + return 0; + } + + if (size > 2 && data[1] != c) { + /* spacing cannot follow an opening emphasis; + * strikethrough and highlight only takes two characters '~~' */ + if (c == '~' || c == '=' || _isspace(data[1]) || (ret = parse_emph1(ob, doc, data + 1, size - 1, c)) == 0) + return 0; + + return ret + 1; + } + + if (size > 3 && data[1] == c && data[2] != c) { + if (_isspace(data[2]) || (ret = parse_emph2(ob, doc, data + 2, size - 2, c)) == 0) + return 0; + + return ret + 2; + } + + if (size > 4 && data[1] == c && data[2] == c && data[3] != c) { + if (c == '~' || c == '=' || _isspace(data[3]) || (ret = parse_emph3(ob, doc, data + 3, size - 3, c)) == 0) + return 0; + + return ret + 3; + } + + return 0; +} + + +/* char_linebreak • '\n' preceded by two spaces (assuming linebreak != 0) */ +static size_t +char_linebreak(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + if (offset < 2 || data[-1] != ' ' || data[-2] != ' ') + return 0; + + /* removing the last space from ob and rendering */ + while (ob->size && ob->data[ob->size - 1] == ' ') + ob->size--; + + return doc->md.linebreak(ob, &doc->data) ? 1 : 0; +} + + +/* char_codespan • '`' parsing a code span (assuming codespan != 0) */ +static size_t +char_codespan(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL }; + size_t end, nb = 0, i, f_begin, f_end; + + /* counting the number of backticks in the delimiter */ + while (nb < size && data[nb] == '`') + nb++; + + /* finding the next delimiter */ + i = 0; + for (end = nb; end < size && i < nb; end++) { + if (data[end] == '`') i++; + else i = 0; + } + + if (i < nb && end >= size) + return 0; /* no matching delimiter */ + + /* trimming outside spaces */ + f_begin = nb; + while (f_begin < end && data[f_begin] == ' ') + f_begin++; + + f_end = end - nb; + while (f_end > nb && data[f_end-1] == ' ') + f_end--; + + /* real code span */ + if (f_begin < f_end) { + work.data = data + f_begin; + work.size = f_end - f_begin; + + if (!doc->md.codespan(ob, &work, &doc->data)) + end = 0; + } else { + if (!doc->md.codespan(ob, 0, &doc->data)) + end = 0; + } + + return end; +} + +/* char_quote • '"' parsing a quote */ +static size_t +char_quote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + size_t end, nq = 0, i, f_begin, f_end; + + /* counting the number of quotes in the delimiter */ + while (nq < size && data[nq] == '"') + nq++; + + /* finding the next delimiter */ + end = nq; + while (1) { + i = end; + end += find_emph_char(data + end, size - end, '"'); + if (end == i) return 0; /* no matching delimiter */ + i = end; + while (end < size && data[end] == '"' && end - i < nq) end++; + if (end - i >= nq) break; + } + + /* trimming outside spaces */ + f_begin = nq; + while (f_begin < end && data[f_begin] == ' ') + f_begin++; + + f_end = end - nq; + while (f_end > nq && data[f_end-1] == ' ') + f_end--; + + /* real quote */ + if (f_begin < f_end) { + hoedown_buffer *work = newbuf(doc, BUFFER_SPAN); + parse_inline(work, doc, data + f_begin, f_end - f_begin); + + if (!doc->md.quote(ob, work, &doc->data)) + end = 0; + popbuf(doc, BUFFER_SPAN); + } else { + if (!doc->md.quote(ob, 0, &doc->data)) + end = 0; + } + + return end; +} + + +/* char_escape • '\\' backslash escape */ +static size_t +char_escape(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + static const char *escape_chars = "\\`*_{}[]()#+-.!:|&<>^~=\"$"; + hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL }; + size_t w; + + if (size > 1) { + if (data[1] == '\\' && (doc->ext_flags & HOEDOWN_EXT_MATH) && + size > 2 && (data[2] == '(' || data[2] == '[')) { + const char *end = (data[2] == '[') ? "\\\\]" : "\\\\)"; + w = parse_math(ob, doc, data, offset, size, end, 3, data[2] == '['); + if (w) return w; + } + + if (strchr(escape_chars, data[1]) == NULL) + return 0; + + if (doc->md.normal_text) { + work.data = data + 1; + work.size = 1; + doc->md.normal_text(ob, &work, &doc->data); + } + else hoedown_buffer_putc(ob, data[1]); + } else if (size == 1) { + hoedown_buffer_putc(ob, data[0]); + } + + return 2; +} + +/* char_entity • '&' escaped when it doesn't belong to an entity */ +/* valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; */ +static size_t +char_entity(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + size_t end = 1; + hoedown_buffer work = { 0, 0, 0, 0, NULL, NULL, NULL }; + + if (end < size && data[end] == '#') + end++; + + while (end < size && isalnum(data[end])) + end++; + + if (end < size && data[end] == ';') + end++; /* real entity */ + else + return 0; /* lone '&' */ + + if (doc->md.entity) { + work.data = data; + work.size = end; + doc->md.entity(ob, &work, &doc->data); + } + else hoedown_buffer_put(ob, data, end); + + return end; +} + +/* char_langle_tag • '<' when tags or autolinks are allowed */ +static size_t +char_langle_tag(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL }; + hoedown_autolink_type altype = HOEDOWN_AUTOLINK_NONE; + size_t end = tag_length(data, size, &altype); + int ret = 0; + + work.data = data; + work.size = end; + + if (end > 2) { + if (doc->md.autolink && altype != HOEDOWN_AUTOLINK_NONE) { + hoedown_buffer *u_link = newbuf(doc, BUFFER_SPAN); + work.data = data + 1; + work.size = end - 2; + unscape_text(u_link, &work); + ret = doc->md.autolink(ob, u_link, altype, &doc->data); + popbuf(doc, BUFFER_SPAN); + } + else if (doc->md.raw_html) + ret = doc->md.raw_html(ob, &work, &doc->data); + } + + if (!ret) return 0; + else return end; +} + +static size_t +char_autolink_www(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + hoedown_buffer *link, *link_url, *link_text; + size_t link_len, rewind; + + if (!doc->md.link || doc->in_link_body) + return 0; + + link = newbuf(doc, BUFFER_SPAN); + + if ((link_len = hoedown_autolink__www(&rewind, link, data, offset, size, HOEDOWN_AUTOLINK_SHORT_DOMAINS)) > 0) { + link_url = newbuf(doc, BUFFER_SPAN); + HOEDOWN_BUFPUTSL(link_url, "http://"); + hoedown_buffer_put(link_url, link->data, link->size); + + ob->size -= rewind; + if (doc->md.normal_text) { + link_text = newbuf(doc, BUFFER_SPAN); + doc->md.normal_text(link_text, link, &doc->data); + doc->md.link(ob, link_text, link_url, NULL, &doc->data); + popbuf(doc, BUFFER_SPAN); + } else { + doc->md.link(ob, link, link_url, NULL, &doc->data); + } + popbuf(doc, BUFFER_SPAN); + } + + popbuf(doc, BUFFER_SPAN); + return link_len; +} + +static size_t +char_autolink_email(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + hoedown_buffer *link; + size_t link_len, rewind; + + if (!doc->md.autolink || doc->in_link_body) + return 0; + + link = newbuf(doc, BUFFER_SPAN); + + if ((link_len = hoedown_autolink__email(&rewind, link, data, offset, size, 0)) > 0) { + ob->size -= rewind; + doc->md.autolink(ob, link, HOEDOWN_AUTOLINK_EMAIL, &doc->data); + } + + popbuf(doc, BUFFER_SPAN); + return link_len; +} + +static size_t +char_autolink_url(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + hoedown_buffer *link; + size_t link_len, rewind; + + if (!doc->md.autolink || doc->in_link_body) + return 0; + + link = newbuf(doc, BUFFER_SPAN); + + if ((link_len = hoedown_autolink__url(&rewind, link, data, offset, size, 0)) > 0) { + ob->size -= rewind; + doc->md.autolink(ob, link, HOEDOWN_AUTOLINK_NORMAL, &doc->data); + } + + popbuf(doc, BUFFER_SPAN); + return link_len; +} + +/* char_link • '[': parsing a link, a footnote or an image */ +static size_t +char_link(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + int is_img = (offset && data[-1] == '!' && !is_escaped(data - offset, offset - 1)); + int is_footnote = (doc->ext_flags & HOEDOWN_EXT_FOOTNOTES && data[1] == '^'); + size_t i = 1, txt_e, link_b = 0, link_e = 0, title_b = 0, title_e = 0; + hoedown_buffer *content = NULL; + hoedown_buffer *link = NULL; + hoedown_buffer *title = NULL; + hoedown_buffer *u_link = NULL; + size_t org_work_size = doc->work_bufs[BUFFER_SPAN].size; + int ret = 0, in_title = 0, qtype = 0; + + /* checking whether the correct renderer exists */ + if ((is_footnote && !doc->md.footnote_ref) || (is_img && !doc->md.image) + || (!is_img && !is_footnote && !doc->md.link)) + goto cleanup; + + /* looking for the matching closing bracket */ + i += find_emph_char(data + i, size - i, ']'); + txt_e = i; + + if (i < size && data[i] == ']') i++; + else goto cleanup; + + /* footnote link */ + if (is_footnote) { + hoedown_buffer id = { NULL, 0, 0, 0, NULL, NULL, NULL }; + struct footnote_ref *fr; + + if (txt_e < 3) + goto cleanup; + + id.data = data + 2; + id.size = txt_e - 2; + + fr = find_footnote_ref(&doc->footnotes_found, id.data, id.size); + + /* mark footnote used */ + if (fr && !fr->is_used) { + if(!add_footnote_ref(&doc->footnotes_used, fr)) + goto cleanup; + fr->is_used = 1; + fr->num = doc->footnotes_used.count; + + /* render */ + if (doc->md.footnote_ref) + ret = doc->md.footnote_ref(ob, fr->num, &doc->data); + } + + goto cleanup; + } + + /* skip any amount of spacing */ + /* (this is much more laxist than original markdown syntax) */ + while (i < size && _isspace(data[i])) + i++; + + /* inline style link */ + if (i < size && data[i] == '(') { + size_t nb_p; + + /* skipping initial spacing */ + i++; + + while (i < size && _isspace(data[i])) + i++; + + link_b = i; + + /* looking for link end: ' " ) */ + /* Count the number of open parenthesis */ + nb_p = 0; + + while (i < size) { + if (data[i] == '\\') i += 2; + else if (data[i] == '(' && i != 0) { + nb_p++; i++; + } + else if (data[i] == ')') { + if (nb_p == 0) break; + else nb_p--; i++; + } else if (i >= 1 && _isspace(data[i-1]) && (data[i] == '\'' || data[i] == '"')) break; + else i++; + } + + if (i >= size) goto cleanup; + link_e = i; + + /* looking for title end if present */ + if (data[i] == '\'' || data[i] == '"') { + qtype = data[i]; + in_title = 1; + i++; + title_b = i; + + while (i < size) { + if (data[i] == '\\') i += 2; + else if (data[i] == qtype) {in_title = 0; i++;} + else if ((data[i] == ')') && !in_title) break; + else i++; + } + + if (i >= size) goto cleanup; + + /* skipping spacing after title */ + title_e = i - 1; + while (title_e > title_b && _isspace(data[title_e])) + title_e--; + + /* checking for closing quote presence */ + if (data[title_e] != '\'' && data[title_e] != '"') { + title_b = title_e = 0; + link_e = i; + } + } + + /* remove spacing at the end of the link */ + while (link_e > link_b && _isspace(data[link_e - 1])) + link_e--; + + /* remove optional angle brackets around the link */ + if (data[link_b] == '<') link_b++; + if (data[link_e - 1] == '>') link_e--; + + /* building escaped link and title */ + if (link_e > link_b) { + link = newbuf(doc, BUFFER_SPAN); + hoedown_buffer_put(link, data + link_b, link_e - link_b); + } + + if (title_e > title_b) { + title = newbuf(doc, BUFFER_SPAN); + hoedown_buffer_put(title, data + title_b, title_e - title_b); + } + + i++; + } + + /* reference style link */ + else if (i < size && data[i] == '[') { + hoedown_buffer *id = newbuf(doc, BUFFER_SPAN); + struct link_ref *lr; + + /* looking for the id */ + i++; + link_b = i; + while (i < size && data[i] != ']') i++; + if (i >= size) goto cleanup; + link_e = i; + + /* finding the link_ref */ + if (link_b == link_e) + replace_spacing(id, data + 1, txt_e - 1); + else + hoedown_buffer_put(id, data + link_b, link_e - link_b); + + lr = find_link_ref(doc->refs, id->data, id->size); + if (!lr) + goto cleanup; + + /* keeping link and title from link_ref */ + link = lr->link; + title = lr->title; + i++; + } + + /* shortcut reference style link */ + else { + hoedown_buffer *id = newbuf(doc, BUFFER_SPAN); + struct link_ref *lr; + + /* crafting the id */ + replace_spacing(id, data + 1, txt_e - 1); + + /* finding the link_ref */ + lr = find_link_ref(doc->refs, id->data, id->size); + if (!lr) + goto cleanup; + + /* keeping link and title from link_ref */ + link = lr->link; + title = lr->title; + + /* rewinding the spacing */ + i = txt_e + 1; + } + + /* building content: img alt is kept, only link content is parsed */ + if (txt_e > 1) { + content = newbuf(doc, BUFFER_SPAN); + if (is_img) { + hoedown_buffer_put(content, data + 1, txt_e - 1); + } else { + /* disable autolinking when parsing inline the + * content of a link */ + doc->in_link_body = 1; + parse_inline(content, doc, data + 1, txt_e - 1); + doc->in_link_body = 0; + } + } + + if (link) { + u_link = newbuf(doc, BUFFER_SPAN); + unscape_text(u_link, link); + } + + /* calling the relevant rendering function */ + if (is_img) { + if (ob->size && ob->data[ob->size - 1] == '!') + ob->size -= 1; + + ret = doc->md.image(ob, u_link, title, content, &doc->data); + } else { + ret = doc->md.link(ob, content, u_link, title, &doc->data); + } + + /* cleanup */ +cleanup: + doc->work_bufs[BUFFER_SPAN].size = (int)org_work_size; + return ret ? i : 0; +} + +static size_t +char_superscript(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + size_t sup_start, sup_len; + hoedown_buffer *sup; + + if (!doc->md.superscript) + return 0; + + if (size < 2) + return 0; + + if (data[1] == '(') { + sup_start = 2; + sup_len = find_emph_char(data + 2, size - 2, ')') + 2; + + if (sup_len == size) + return 0; + } else { + sup_start = sup_len = 1; + + while (sup_len < size && !_isspace(data[sup_len])) + sup_len++; + } + + if (sup_len - sup_start == 0) + return (sup_start == 2) ? 3 : 0; + + sup = newbuf(doc, BUFFER_SPAN); + parse_inline(sup, doc, data + sup_start, sup_len - sup_start); + doc->md.superscript(ob, sup, &doc->data); + popbuf(doc, BUFFER_SPAN); + + return (sup_start == 2) ? sup_len + 1 : sup_len; +} + +static size_t +char_math(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t offset, size_t size) +{ + /* double dollar */ + if (size > 1 && data[1] == '$') + return parse_math(ob, doc, data, offset, size, "$$", 2, 1); + + /* single dollar allowed only with MATH_EXPLICIT flag */ + if (doc->ext_flags & HOEDOWN_EXT_MATH_EXPLICIT) + return parse_math(ob, doc, data, offset, size, "$", 1, 0); + + return 0; +} + +/********************************* + * BLOCK-LEVEL PARSING FUNCTIONS * + *********************************/ + +/* is_empty • returns the line length when it is empty, 0 otherwise */ +static size_t +is_empty(const uint8_t *data, size_t size) +{ + size_t i; + + for (i = 0; i < size && data[i] != '\n'; i++) + if (data[i] != ' ') + return 0; + + return i + 1; +} + +/* is_hrule • returns whether a line is a horizontal rule */ +static int +is_hrule(uint8_t *data, size_t size) +{ + size_t i = 0, n = 0; + uint8_t c; + + /* skipping initial spaces */ + if (size < 3) return 0; + if (data[0] == ' ') { i++; + if (data[1] == ' ') { i++; + if (data[2] == ' ') { i++; } } } + + /* looking at the hrule uint8_t */ + if (i + 2 >= size + || (data[i] != '*' && data[i] != '-' && data[i] != '_')) + return 0; + c = data[i]; + + /* the whole line must be the char or space */ + while (i < size && data[i] != '\n') { + if (data[i] == c) n++; + else if (data[i] != ' ') + return 0; + + i++; + } + + return n >= 3; +} + +/* check if a line is a code fence; return the + * end of the code fence. if passed, width of + * the fence rule and character will be returned */ +static size_t +is_codefence(uint8_t *data, size_t size, size_t *width, uint8_t *chr) +{ + size_t i = 0, n = 1; + uint8_t c; + + /* skipping initial spaces */ + if (size < 3) + return 0; + + if (data[0] == ' ') { i++; + if (data[1] == ' ') { i++; + if (data[2] == ' ') { i++; } } } + + /* looking at the hrule uint8_t */ + c = data[i]; + if (i + 2 >= size || !(c=='~' || c=='`')) + return 0; + + /* the fence must be that same character */ + while (++i < size && data[i] == c) + ++n; + + if (n < 3) + return 0; + + if (width) *width = n; + if (chr) *chr = c; + return i; +} + +/* expects single line, checks if it's a codefence and extracts language */ +static size_t +parse_codefence(uint8_t *data, size_t size, hoedown_buffer *lang, size_t *width, uint8_t *chr) +{ + size_t i, w, lang_start; + + i = w = is_codefence(data, size, width, chr); + if (i == 0) + return 0; + + while (i < size && _isspace(data[i])) + i++; + + lang_start = i; + + while (i < size && !_isspace(data[i])) + i++; + + lang->data = data + lang_start; + lang->size = i - lang_start; + + /* Avoid parsing a codespan as a fence */ + i = lang_start + 2; + while (i < size && !(data[i] == *chr && data[i-1] == *chr && data[i-2] == *chr)) i++; + if (i < size) return 0; + + return w; +} + +/* is_atxheader • returns whether the line is a hash-prefixed header */ +static int +is_atxheader(hoedown_document *doc, uint8_t *data, size_t size) +{ + if (data[0] != '#') + return 0; + + if (doc->ext_flags & HOEDOWN_EXT_SPACE_HEADERS) { + size_t level = 0; + + while (level < size && level < 6 && data[level] == '#') + level++; + + if (level < size && data[level] != ' ') + return 0; + } + + return 1; +} + +/* is_headerline • returns whether the line is a setext-style hdr underline */ +static int +is_headerline(uint8_t *data, size_t size) +{ + size_t i = 0; + + /* test of level 1 header */ + if (data[i] == '=') { + for (i = 1; i < size && data[i] == '='; i++); + while (i < size && data[i] == ' ') i++; + return (i >= size || data[i] == '\n') ? 1 : 0; } + + /* test of level 2 header */ + if (data[i] == '-') { + for (i = 1; i < size && data[i] == '-'; i++); + while (i < size && data[i] == ' ') i++; + return (i >= size || data[i] == '\n') ? 2 : 0; } + + return 0; +} + +static int +is_next_headerline(uint8_t *data, size_t size) +{ + size_t i = 0; + + while (i < size && data[i] != '\n') + i++; + + if (++i >= size) + return 0; + + return is_headerline(data + i, size - i); +} + +/* prefix_quote • returns blockquote prefix length */ +static size_t +prefix_quote(uint8_t *data, size_t size) +{ + size_t i = 0; + if (i < size && data[i] == ' ') i++; + if (i < size && data[i] == ' ') i++; + if (i < size && data[i] == ' ') i++; + + if (i < size && data[i] == '>') { + if (i + 1 < size && data[i + 1] == ' ') + return i + 2; + + return i + 1; + } + + return 0; +} + +/* prefix_code • returns prefix length for block code*/ +static size_t +prefix_code(uint8_t *data, size_t size) +{ + if (size > 3 && data[0] == ' ' && data[1] == ' ' + && data[2] == ' ' && data[3] == ' ') return 4; + + return 0; +} + +/* prefix_oli • returns ordered list item prefix */ +static size_t +prefix_oli(uint8_t *data, size_t size) +{ + size_t i = 0; + + if (i < size && data[i] == ' ') i++; + if (i < size && data[i] == ' ') i++; + if (i < size && data[i] == ' ') i++; + + if (i >= size || data[i] < '0' || data[i] > '9') + return 0; + + while (i < size && data[i] >= '0' && data[i] <= '9') + i++; + + if (i + 1 >= size || data[i] != '.' || data[i + 1] != ' ') + return 0; + + if (is_next_headerline(data + i, size - i)) + return 0; + + return i + 2; +} + +/* prefix_uli • returns ordered list item prefix */ +static size_t +prefix_uli(uint8_t *data, size_t size) +{ + size_t i = 0; + + if (i < size && data[i] == ' ') i++; + if (i < size && data[i] == ' ') i++; + if (i < size && data[i] == ' ') i++; + + if (i + 1 >= size || + (data[i] != '*' && data[i] != '+' && data[i] != '-') || + data[i + 1] != ' ') + return 0; + + if (is_next_headerline(data + i, size - i)) + return 0; + + return i + 2; +} + + +/* parse_block • parsing of one block, returning next uint8_t to parse */ +static void parse_block(hoedown_buffer *ob, hoedown_document *doc, + uint8_t *data, size_t size); + + +/* parse_blockquote • handles parsing of a blockquote fragment */ +static size_t +parse_blockquote(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) +{ + size_t beg, end = 0, pre, work_size = 0; + uint8_t *work_data = 0; + hoedown_buffer *out = 0; + + out = newbuf(doc, BUFFER_BLOCK); + beg = 0; + while (beg < size) { + for (end = beg + 1; end < size && data[end - 1] != '\n'; end++); + + pre = prefix_quote(data + beg, end - beg); + + if (pre) + beg += pre; /* skipping prefix */ + + /* empty line followed by non-quote line */ + else if (is_empty(data + beg, end - beg) && + (end >= size || (prefix_quote(data + end, size - end) == 0 && + !is_empty(data + end, size - end)))) + break; + + if (beg < end) { /* copy into the in-place working buffer */ + /* hoedown_buffer_put(work, data + beg, end - beg); */ + if (!work_data) + work_data = data + beg; + else if (data + beg != work_data + work_size) + memmove(work_data + work_size, data + beg, end - beg); + work_size += end - beg; + } + beg = end; + } + + parse_block(out, doc, work_data, work_size); + if (doc->md.blockquote) + doc->md.blockquote(ob, out, &doc->data); + popbuf(doc, BUFFER_BLOCK); + return end; +} + +static size_t +parse_htmlblock(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, int do_render); + +/* parse_blockquote • handles parsing of a regular paragraph */ +static size_t +parse_paragraph(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) +{ + hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL }; + size_t i = 0, end = 0; + int level = 0; + + work.data = data; + + while (i < size) { + for (end = i + 1; end < size && data[end - 1] != '\n'; end++) /* empty */; + + if (is_empty(data + i, size - i)) + break; + + if ((level = is_headerline(data + i, size - i)) != 0) + break; + + if (is_atxheader(doc, data + i, size - i) || + is_hrule(data + i, size - i) || + prefix_quote(data + i, size - i)) { + end = i; + break; + } + + i = end; + } + + work.size = i; + while (work.size && data[work.size - 1] == '\n') + work.size--; + + if (!level) { + hoedown_buffer *tmp = newbuf(doc, BUFFER_BLOCK); + parse_inline(tmp, doc, work.data, work.size); + if (doc->md.paragraph) + doc->md.paragraph(ob, tmp, &doc->data); + popbuf(doc, BUFFER_BLOCK); + } else { + hoedown_buffer *header_work; + + if (work.size) { + size_t beg; + i = work.size; + work.size -= 1; + + while (work.size && data[work.size] != '\n') + work.size -= 1; + + beg = work.size + 1; + while (work.size && data[work.size - 1] == '\n') + work.size -= 1; + + if (work.size > 0) { + hoedown_buffer *tmp = newbuf(doc, BUFFER_BLOCK); + parse_inline(tmp, doc, work.data, work.size); + + if (doc->md.paragraph) + doc->md.paragraph(ob, tmp, &doc->data); + + popbuf(doc, BUFFER_BLOCK); + work.data += beg; + work.size = i - beg; + } + else work.size = i; + } + + header_work = newbuf(doc, BUFFER_SPAN); + parse_inline(header_work, doc, work.data, work.size); + + if (doc->md.header) + doc->md.header(ob, header_work, (int)level, &doc->data); + + popbuf(doc, BUFFER_SPAN); + } + + return end; +} + +/* parse_fencedcode • handles parsing of a block-level code fragment */ +static size_t +parse_fencedcode(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) +{ + hoedown_buffer text = { 0, 0, 0, 0, NULL, NULL, NULL }; + hoedown_buffer lang = { 0, 0, 0, 0, NULL, NULL, NULL }; + size_t i = 0, text_start, line_start; + size_t w, w2; + size_t width, width2; + uint8_t chr, chr2; + + /* parse codefence line */ + while (i < size && data[i] != '\n') + i++; + + w = parse_codefence(data, i, &lang, &width, &chr); + if (!w) + return 0; + + /* search for end */ + i++; + text_start = i; + while ((line_start = i) < size) { + while (i < size && data[i] != '\n') + i++; + + w2 = is_codefence(data + line_start, i - line_start, &width2, &chr2); + if (w == w2 && width == width2 && chr == chr2 && + is_empty(data + (line_start+w), i - (line_start+w))) + break; + + i++; + } + + text.data = data + text_start; + text.size = line_start - text_start; + + if (doc->md.blockcode) + doc->md.blockcode(ob, text.size ? &text : NULL, lang.size ? &lang : NULL, &doc->data); + + return i; +} + +static size_t +parse_blockcode(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) +{ + size_t beg, end, pre; + hoedown_buffer *work = 0; + + work = newbuf(doc, BUFFER_BLOCK); + + beg = 0; + while (beg < size) { + for (end = beg + 1; end < size && data[end - 1] != '\n'; end++) {}; + pre = prefix_code(data + beg, end - beg); + + if (pre) + beg += pre; /* skipping prefix */ + else if (!is_empty(data + beg, end - beg)) + /* non-empty non-prefixed line breaks the pre */ + break; + + if (beg < end) { + /* verbatim copy to the working buffer, + escaping entities */ + if (is_empty(data + beg, end - beg)) + hoedown_buffer_putc(work, '\n'); + else hoedown_buffer_put(work, data + beg, end - beg); + } + beg = end; + } + + while (work->size && work->data[work->size - 1] == '\n') + work->size -= 1; + + hoedown_buffer_putc(work, '\n'); + + if (doc->md.blockcode) + doc->md.blockcode(ob, work, NULL, &doc->data); + + popbuf(doc, BUFFER_BLOCK); + return beg; +} + +/* parse_listitem • parsing of a single list item */ +/* assuming initial prefix is already removed */ +static size_t +parse_listitem(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, hoedown_list_flags *flags) +{ + hoedown_buffer *work = 0, *inter = 0; + size_t beg = 0, end, pre, sublist = 0, orgpre = 0, i; + int in_empty = 0, has_inside_empty = 0, in_fence = 0; + + /* keeping track of the first indentation prefix */ + while (orgpre < 3 && orgpre < size && data[orgpre] == ' ') + orgpre++; + + beg = prefix_uli(data, size); + if (!beg) + beg = prefix_oli(data, size); + + if (!beg) + return 0; + + /* skipping to the beginning of the following line */ + end = beg; + while (end < size && data[end - 1] != '\n') + end++; + + /* getting working buffers */ + work = newbuf(doc, BUFFER_SPAN); + inter = newbuf(doc, BUFFER_SPAN); + + /* putting the first line into the working buffer */ + hoedown_buffer_put(work, data + beg, end - beg); + beg = end; + + /* process the following lines */ + while (beg < size) { + size_t has_next_uli = 0, has_next_oli = 0; + + end++; + + while (end < size && data[end - 1] != '\n') + end++; + + /* process an empty line */ + if (is_empty(data + beg, end - beg)) { + in_empty = 1; + beg = end; + continue; + } + + /* calculating the indentation */ + i = 0; + while (i < 4 && beg + i < end && data[beg + i] == ' ') + i++; + + pre = i; + + if (doc->ext_flags & HOEDOWN_EXT_FENCED_CODE) { + if (is_codefence(data + beg + i, end - beg - i, NULL, NULL)) + in_fence = !in_fence; + } + + /* Only check for new list items if we are **not** inside + * a fenced code block */ + if (!in_fence) { + has_next_uli = prefix_uli(data + beg + i, end - beg - i); + has_next_oli = prefix_oli(data + beg + i, end - beg - i); + } + + /* checking for a new item */ + if ((has_next_uli && !is_hrule(data + beg + i, end - beg - i)) || has_next_oli) { + if (in_empty) + has_inside_empty = 1; + + /* the following item must have the same (or less) indentation */ + if (pre <= orgpre) { + /* if the following item has different list type, we end this list */ + if (in_empty && ( + ((*flags & HOEDOWN_LIST_ORDERED) && has_next_uli) || + (!(*flags & HOEDOWN_LIST_ORDERED) && has_next_oli))) + *flags |= HOEDOWN_LI_END; + + break; + } + + if (!sublist) + sublist = work->size; + } + /* joining only indented stuff after empty lines; + * note that now we only require 1 space of indentation + * to continue a list */ + else if (in_empty && pre == 0) { + *flags |= HOEDOWN_LI_END; + break; + } + + if (in_empty) { + hoedown_buffer_putc(work, '\n'); + has_inside_empty = 1; + in_empty = 0; + } + + /* adding the line without prefix into the working buffer */ + hoedown_buffer_put(work, data + beg + i, end - beg - i); + beg = end; + } + + /* render of li contents */ + if (has_inside_empty) + *flags |= HOEDOWN_LI_BLOCK; + + if (*flags & HOEDOWN_LI_BLOCK) { + /* intermediate render of block li */ + if (sublist && sublist < work->size) { + parse_block(inter, doc, work->data, sublist); + parse_block(inter, doc, work->data + sublist, work->size - sublist); + } + else + parse_block(inter, doc, work->data, work->size); + } else { + /* intermediate render of inline li */ + if (sublist && sublist < work->size) { + parse_inline(inter, doc, work->data, sublist); + parse_block(inter, doc, work->data + sublist, work->size - sublist); + } + else + parse_inline(inter, doc, work->data, work->size); + } + + /* render of li itself */ + if (doc->md.listitem) + doc->md.listitem(ob, inter, *flags, &doc->data); + + popbuf(doc, BUFFER_SPAN); + popbuf(doc, BUFFER_SPAN); + return beg; +} + + +/* parse_list • parsing ordered or unordered list block */ +static size_t +parse_list(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, hoedown_list_flags flags) +{ + hoedown_buffer *work = 0; + size_t i = 0, j; + + work = newbuf(doc, BUFFER_BLOCK); + + while (i < size) { + j = parse_listitem(work, doc, data + i, size - i, &flags); + i += j; + + if (!j || (flags & HOEDOWN_LI_END)) + break; + } + + if (doc->md.list) + doc->md.list(ob, work, flags, &doc->data); + popbuf(doc, BUFFER_BLOCK); + return i; +} + +/* parse_atxheader • parsing of atx-style headers */ +static size_t +parse_atxheader(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) +{ + size_t level = 0; + size_t i, end, skip; + + while (level < size && level < 6 && data[level] == '#') + level++; + + for (i = level; i < size && data[i] == ' '; i++); + + for (end = i; end < size && data[end] != '\n'; end++); + skip = end; + + while (end && data[end - 1] == '#') + end--; + + while (end && data[end - 1] == ' ') + end--; + + if (end > i) { + hoedown_buffer *work = newbuf(doc, BUFFER_SPAN); + + parse_inline(work, doc, data + i, end - i); + + if (doc->md.header) + doc->md.header(ob, work, (int)level, &doc->data); + + popbuf(doc, BUFFER_SPAN); + } + + return skip; +} + +/* parse_footnote_def • parse a single footnote definition */ +static void +parse_footnote_def(hoedown_buffer *ob, hoedown_document *doc, unsigned int num, uint8_t *data, size_t size) +{ + hoedown_buffer *work = 0; + work = newbuf(doc, BUFFER_SPAN); + + parse_block(work, doc, data, size); + + if (doc->md.footnote_def) + doc->md.footnote_def(ob, work, num, &doc->data); + popbuf(doc, BUFFER_SPAN); +} + +/* parse_footnote_list • render the contents of the footnotes */ +static void +parse_footnote_list(hoedown_buffer *ob, hoedown_document *doc, struct footnote_list *footnotes) +{ + hoedown_buffer *work = 0; + struct footnote_item *item; + struct footnote_ref *ref; + + if (footnotes->count == 0) + return; + + work = newbuf(doc, BUFFER_BLOCK); + + item = footnotes->head; + while (item) { + ref = item->ref; + parse_footnote_def(work, doc, ref->num, ref->contents->data, ref->contents->size); + item = item->next; + } + + if (doc->md.footnotes) + doc->md.footnotes(ob, work, &doc->data); + popbuf(doc, BUFFER_BLOCK); +} + +/* htmlblock_is_end • check for end of HTML block : ( *)\n */ +/* returns tag length on match, 0 otherwise */ +/* assumes data starts with "<" */ +static size_t +htmlblock_is_end( + const char *tag, + size_t tag_len, + hoedown_document *doc, + uint8_t *data, + size_t size) +{ + size_t i = tag_len + 3, w; + + /* try to match the end tag */ + /* note: we're not considering tags like "" which are still valid */ + if (i > size || + data[1] != '/' || + strncasecmp((char *)data + 2, tag, tag_len) != 0 || + data[tag_len + 2] != '>') + return 0; + + /* rest of the line must be empty */ + if ((w = is_empty(data + i, size - i)) == 0 && i < size) + return 0; + + return i + w; +} + +/* htmlblock_find_end • try to find HTML block ending tag */ +/* returns the length on match, 0 otherwise */ +static size_t +htmlblock_find_end( + const char *tag, + size_t tag_len, + hoedown_document *doc, + uint8_t *data, + size_t size) +{ + size_t i = 0, w; + + while (1) { + while (i < size && data[i] != '<') i++; + if (i >= size) return 0; + + w = htmlblock_is_end(tag, tag_len, doc, data + i, size - i); + if (w) return i + w; + i++; + } +} + +/* htmlblock_find_end_strict • try to find end of HTML block in strict mode */ +/* (it must be an unindented line, and have a blank line afterwads) */ +/* returns the length on match, 0 otherwise */ +static size_t +htmlblock_find_end_strict( + const char *tag, + size_t tag_len, + hoedown_document *doc, + uint8_t *data, + size_t size) +{ + size_t i = 0, mark; + + while (1) { + mark = i; + while (i < size && data[i] != '\n') i++; + if (i < size) i++; + if (i == mark) return 0; + + if (data[mark] == ' ' && mark > 0) continue; + mark += htmlblock_find_end(tag, tag_len, doc, data + mark, i - mark); + if (mark == i && (is_empty(data + i, size - i) || i >= size)) break; + } + + return i; +} + +/* parse_htmlblock • parsing of inline HTML block */ +static size_t +parse_htmlblock(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size, int do_render) +{ + hoedown_buffer work = { NULL, 0, 0, 0, NULL, NULL, NULL }; + size_t i, j = 0, tag_len, tag_end; + const char *curtag = NULL; + + work.data = data; + + /* identification of the opening tag */ + if (size < 2 || data[0] != '<') + return 0; + + i = 1; + while (i < size && data[i] != '>' && data[i] != ' ') + i++; + + if (i < size) + curtag = hoedown_find_block_tag((char *)data + 1, (int)i - 1); + + /* handling of special cases */ + if (!curtag) { + + /* HTML comment, laxist form */ + if (size > 5 && data[1] == '!' && data[2] == '-' && data[3] == '-') { + i = 5; + + while (i < size && !(data[i - 2] == '-' && data[i - 1] == '-' && data[i] == '>')) + i++; + + i++; + + if (i < size) + j = is_empty(data + i, size - i); + + if (j) { + work.size = i + j; + if (do_render && doc->md.blockhtml) + doc->md.blockhtml(ob, &work, &doc->data); + return work.size; + } + } + + /* HR, which is the only self-closing block tag considered */ + if (size > 4 && (data[1] == 'h' || data[1] == 'H') && (data[2] == 'r' || data[2] == 'R')) { + i = 3; + while (i < size && data[i] != '>') + i++; + + if (i + 1 < size) { + i++; + j = is_empty(data + i, size - i); + if (j) { + work.size = i + j; + if (do_render && doc->md.blockhtml) + doc->md.blockhtml(ob, &work, &doc->data); + return work.size; + } + } + } + + /* no special case recognised */ + return 0; + } + + /* looking for a matching closing tag in strict mode */ + tag_len = strlen(curtag); + tag_end = htmlblock_find_end_strict(curtag, tag_len, doc, data, size); + + /* if not found, trying a second pass looking for indented match */ + /* but not if tag is "ins" or "del" (following original Markdown.pl) */ + if (!tag_end && strcmp(curtag, "ins") != 0 && strcmp(curtag, "del") != 0) + tag_end = htmlblock_find_end(curtag, tag_len, doc, data, size); + + if (!tag_end) + return 0; + + /* the end of the block has been found */ + work.size = tag_end; + if (do_render && doc->md.blockhtml) + doc->md.blockhtml(ob, &work, &doc->data); + + return tag_end; +} + +static void +parse_table_row( + hoedown_buffer *ob, + hoedown_document *doc, + uint8_t *data, + size_t size, + size_t columns, + hoedown_table_flags *col_data, + hoedown_table_flags header_flag) +{ + size_t i = 0, col, len; + hoedown_buffer *row_work = 0; + + if (!doc->md.table_cell || !doc->md.table_row) + return; + + row_work = newbuf(doc, BUFFER_SPAN); + + if (i < size && data[i] == '|') + i++; + + for (col = 0; col < columns && i < size; ++col) { + size_t cell_start, cell_end; + hoedown_buffer *cell_work; + + cell_work = newbuf(doc, BUFFER_SPAN); + + while (i < size && _isspace(data[i])) + i++; + + cell_start = i; + + len = find_emph_char(data + i, size - i, '|'); + i += len ? len : size - i; + + cell_end = i - 1; + + while (cell_end > cell_start && _isspace(data[cell_end])) + cell_end--; + + parse_inline(cell_work, doc, data + cell_start, 1 + cell_end - cell_start); + doc->md.table_cell(row_work, cell_work, col_data[col] | header_flag, &doc->data); + + popbuf(doc, BUFFER_SPAN); + i++; + } + + for (; col < columns; ++col) { + hoedown_buffer empty_cell = { 0, 0, 0, 0, NULL, NULL, NULL }; + doc->md.table_cell(row_work, &empty_cell, col_data[col] | header_flag, &doc->data); + } + + doc->md.table_row(ob, row_work, &doc->data); + + popbuf(doc, BUFFER_SPAN); +} + +static size_t +parse_table_header( + hoedown_buffer *ob, + hoedown_document *doc, + uint8_t *data, + size_t size, + size_t *columns, + hoedown_table_flags **column_data) +{ + int pipes; + size_t i = 0, col, header_end, under_end; + + pipes = 0; + while (i < size && data[i] != '\n') + if (data[i++] == '|') + pipes++; + + if (i == size || pipes == 0) + return 0; + + header_end = i; + + while (header_end > 0 && _isspace(data[header_end - 1])) + header_end--; + + if (data[0] == '|') + pipes--; + + if (header_end && data[header_end - 1] == '|') + pipes--; + + if (pipes < 0) + return 0; + + *columns = pipes + 1; + *column_data = hoedown_calloc(*columns, sizeof(hoedown_table_flags)); + + /* Parse the header underline */ + i++; + if (i < size && data[i] == '|') + i++; + + under_end = i; + while (under_end < size && data[under_end] != '\n') + under_end++; + + for (col = 0; col < *columns && i < under_end; ++col) { + size_t dashes = 0; + + while (i < under_end && data[i] == ' ') + i++; + + if (data[i] == ':') { + i++; (*column_data)[col] |= HOEDOWN_TABLE_ALIGN_LEFT; + dashes++; + } + + while (i < under_end && data[i] == '-') { + i++; dashes++; + } + + if (i < under_end && data[i] == ':') { + i++; (*column_data)[col] |= HOEDOWN_TABLE_ALIGN_RIGHT; + dashes++; + } + + while (i < under_end && data[i] == ' ') + i++; + + if (i < under_end && data[i] != '|' && data[i] != '+') + break; + + if (dashes < 3) + break; + + i++; + } + + if (col < *columns) + return 0; + + parse_table_row( + ob, doc, data, + header_end, + *columns, + *column_data, + HOEDOWN_TABLE_HEADER + ); + + return under_end + 1; +} + +static size_t +parse_table( + hoedown_buffer *ob, + hoedown_document *doc, + uint8_t *data, + size_t size) +{ + size_t i; + + hoedown_buffer *work = 0; + hoedown_buffer *header_work = 0; + hoedown_buffer *body_work = 0; + + size_t columns; + hoedown_table_flags *col_data = NULL; + + work = newbuf(doc, BUFFER_BLOCK); + header_work = newbuf(doc, BUFFER_SPAN); + body_work = newbuf(doc, BUFFER_BLOCK); + + i = parse_table_header(header_work, doc, data, size, &columns, &col_data); + if (i > 0) { + + while (i < size) { + size_t row_start; + int pipes = 0; + + row_start = i; + + while (i < size && data[i] != '\n') + if (data[i++] == '|') + pipes++; + + if (pipes == 0 || i == size) { + i = row_start; + break; + } + + parse_table_row( + body_work, + doc, + data + row_start, + i - row_start, + columns, + col_data, 0 + ); + + i++; + } + + if (doc->md.table_header) + doc->md.table_header(work, header_work, &doc->data); + + if (doc->md.table_body) + doc->md.table_body(work, body_work, &doc->data); + + if (doc->md.table) + doc->md.table(ob, work, &doc->data); + } + + free(col_data); + popbuf(doc, BUFFER_SPAN); + popbuf(doc, BUFFER_BLOCK); + popbuf(doc, BUFFER_BLOCK); + return i; +} + +/* parse_block • parsing of one block, returning next uint8_t to parse */ +static void +parse_block(hoedown_buffer *ob, hoedown_document *doc, uint8_t *data, size_t size) +{ + size_t beg, end, i; + uint8_t *txt_data; + beg = 0; + + if (doc->work_bufs[BUFFER_SPAN].size + + doc->work_bufs[BUFFER_BLOCK].size > doc->max_nesting) + return; + + while (beg < size) { + txt_data = data + beg; + end = size - beg; + + if (is_atxheader(doc, txt_data, end)) + beg += parse_atxheader(ob, doc, txt_data, end); + + else if (data[beg] == '<' && doc->md.blockhtml && + (i = parse_htmlblock(ob, doc, txt_data, end, 1)) != 0) + beg += i; + + else if ((i = is_empty(txt_data, end)) != 0) + beg += i; + + else if (is_hrule(txt_data, end)) { + if (doc->md.hrule) + doc->md.hrule(ob, &doc->data); + + while (beg < size && data[beg] != '\n') + beg++; + + beg++; + } + + else if ((doc->ext_flags & HOEDOWN_EXT_FENCED_CODE) != 0 && + (i = parse_fencedcode(ob, doc, txt_data, end)) != 0) + beg += i; + + else if ((doc->ext_flags & HOEDOWN_EXT_TABLES) != 0 && + (i = parse_table(ob, doc, txt_data, end)) != 0) + beg += i; + + else if (prefix_quote(txt_data, end)) + beg += parse_blockquote(ob, doc, txt_data, end); + + else if (!(doc->ext_flags & HOEDOWN_EXT_DISABLE_INDENTED_CODE) && prefix_code(txt_data, end)) + beg += parse_blockcode(ob, doc, txt_data, end); + + else if (prefix_uli(txt_data, end)) + beg += parse_list(ob, doc, txt_data, end, 0); + + else if (prefix_oli(txt_data, end)) + beg += parse_list(ob, doc, txt_data, end, HOEDOWN_LIST_ORDERED); + + else + beg += parse_paragraph(ob, doc, txt_data, end); + } +} + + + +/********************* + * REFERENCE PARSING * + *********************/ + +/* is_footnote • returns whether a line is a footnote definition or not */ +static int +is_footnote(const uint8_t *data, size_t beg, size_t end, size_t *last, struct footnote_list *list) +{ + size_t i = 0; + hoedown_buffer *contents = 0; + size_t ind = 0; + int in_empty = 0; + size_t start = 0; + + size_t id_offset, id_end; + + /* up to 3 optional leading spaces */ + if (beg + 3 >= end) return 0; + if (data[beg] == ' ') { i = 1; + if (data[beg + 1] == ' ') { i = 2; + if (data[beg + 2] == ' ') { i = 3; + if (data[beg + 3] == ' ') return 0; } } } + i += beg; + + /* id part: caret followed by anything between brackets */ + if (data[i] != '[') return 0; + i++; + if (i >= end || data[i] != '^') return 0; + i++; + id_offset = i; + while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']') + i++; + if (i >= end || data[i] != ']') return 0; + id_end = i; + + /* spacer: colon (space | tab)* newline? (space | tab)* */ + i++; + if (i >= end || data[i] != ':') return 0; + i++; + + /* getting content buffer */ + contents = hoedown_buffer_new(64); + + start = i; + + /* process lines similar to a list item */ + while (i < end) { + while (i < end && data[i] != '\n' && data[i] != '\r') i++; + + /* process an empty line */ + if (is_empty(data + start, i - start)) { + in_empty = 1; + if (i < end && (data[i] == '\n' || data[i] == '\r')) { + i++; + if (i < end && data[i] == '\n' && data[i - 1] == '\r') i++; + } + start = i; + continue; + } + + /* calculating the indentation */ + ind = 0; + while (ind < 4 && start + ind < end && data[start + ind] == ' ') + ind++; + + /* joining only indented stuff after empty lines; + * note that now we only require 1 space of indentation + * to continue, just like lists */ + if (ind == 0) { + if (start == id_end + 2 && data[start] == '\t') {} + else break; + } + else if (in_empty) { + hoedown_buffer_putc(contents, '\n'); + } + + in_empty = 0; + + /* adding the line into the content buffer */ + hoedown_buffer_put(contents, data + start + ind, i - start - ind); + /* add carriage return */ + if (i < end) { + hoedown_buffer_putc(contents, '\n'); + if (i < end && (data[i] == '\n' || data[i] == '\r')) { + i++; + if (i < end && data[i] == '\n' && data[i - 1] == '\r') i++; + } + } + start = i; + } + + if (last) + *last = start; + + if (list) { + struct footnote_ref *ref; + ref = create_footnote_ref(list, data + id_offset, id_end - id_offset); + if (!ref) + return 0; + if (!add_footnote_ref(list, ref)) { + free_footnote_ref(ref); + return 0; + } + ref->contents = contents; + } + + return 1; +} + +/* is_ref • returns whether a line is a reference or not */ +static int +is_ref(const uint8_t *data, size_t beg, size_t end, size_t *last, struct link_ref **refs) +{ +/* int n; */ + size_t i = 0; + size_t id_offset, id_end; + size_t link_offset, link_end; + size_t title_offset, title_end; + size_t line_end; + + /* up to 3 optional leading spaces */ + if (beg + 3 >= end) return 0; + if (data[beg] == ' ') { i = 1; + if (data[beg + 1] == ' ') { i = 2; + if (data[beg + 2] == ' ') { i = 3; + if (data[beg + 3] == ' ') return 0; } } } + i += beg; + + /* id part: anything but a newline between brackets */ + if (data[i] != '[') return 0; + i++; + id_offset = i; + while (i < end && data[i] != '\n' && data[i] != '\r' && data[i] != ']') + i++; + if (i >= end || data[i] != ']') return 0; + id_end = i; + + /* spacer: colon (space | tab)* newline? (space | tab)* */ + i++; + if (i >= end || data[i] != ':') return 0; + i++; + while (i < end && data[i] == ' ') i++; + if (i < end && (data[i] == '\n' || data[i] == '\r')) { + i++; + if (i < end && data[i] == '\r' && data[i - 1] == '\n') i++; } + while (i < end && data[i] == ' ') i++; + if (i >= end) return 0; + + /* link: spacing-free sequence, optionally between angle brackets */ + if (data[i] == '<') + i++; + + link_offset = i; + + while (i < end && data[i] != ' ' && data[i] != '\n' && data[i] != '\r') + i++; + + if (data[i - 1] == '>') link_end = i - 1; + else link_end = i; + + /* optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) */ + while (i < end && data[i] == ' ') i++; + if (i < end && data[i] != '\n' && data[i] != '\r' + && data[i] != '\'' && data[i] != '"' && data[i] != '(') + return 0; + line_end = 0; + /* computing end-of-line */ + if (i >= end || data[i] == '\r' || data[i] == '\n') line_end = i; + if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r') + line_end = i + 1; + + /* optional (space|tab)* spacer after a newline */ + if (line_end) { + i = line_end + 1; + while (i < end && data[i] == ' ') i++; } + + /* optional title: any non-newline sequence enclosed in '"() + alone on its line */ + title_offset = title_end = 0; + if (i + 1 < end + && (data[i] == '\'' || data[i] == '"' || data[i] == '(')) { + i++; + title_offset = i; + /* looking for EOL */ + while (i < end && data[i] != '\n' && data[i] != '\r') i++; + if (i + 1 < end && data[i] == '\n' && data[i + 1] == '\r') + title_end = i + 1; + else title_end = i; + /* stepping back */ + i -= 1; + while (i > title_offset && data[i] == ' ') + i -= 1; + if (i > title_offset + && (data[i] == '\'' || data[i] == '"' || data[i] == ')')) { + line_end = title_end; + title_end = i; } } + + if (!line_end || link_end == link_offset) + return 0; /* garbage after the link empty link */ + + /* a valid ref has been found, filling-in return structures */ + if (last) + *last = line_end; + + if (refs) { + struct link_ref *ref; + + ref = add_link_ref(refs, data + id_offset, id_end - id_offset); + if (!ref) + return 0; + + ref->link = hoedown_buffer_new(link_end - link_offset); + hoedown_buffer_put(ref->link, data + link_offset, link_end - link_offset); + + if (title_end > title_offset) { + ref->title = hoedown_buffer_new(title_end - title_offset); + hoedown_buffer_put(ref->title, data + title_offset, title_end - title_offset); + } + } + + return 1; +} + +static void expand_tabs(hoedown_buffer *ob, const uint8_t *line, size_t size) +{ + /* This code makes two assumptions: + * - Input is valid UTF-8. (Any byte with top two bits 10 is skipped, + * whether or not it is a valid UTF-8 continuation byte.) + * - Input contains no combining characters. (Combining characters + * should be skipped but are not.) + */ + size_t i = 0, tab = 0; + + while (i < size) { + size_t org = i; + + while (i < size && line[i] != '\t') { + /* ignore UTF-8 continuation bytes */ + if ((line[i] & 0xc0) != 0x80) + tab++; + i++; + } + + if (i > org) + hoedown_buffer_put(ob, line + org, i - org); + + if (i >= size) + break; + + do { + hoedown_buffer_putc(ob, ' '); tab++; + } while (tab % 4); + + i++; + } +} + +/********************** + * EXPORTED FUNCTIONS * + **********************/ + +hoedown_document * +hoedown_document_new( + const hoedown_renderer *renderer, + hoedown_extensions extensions, + size_t max_nesting) +{ + hoedown_document *doc = NULL; + + assert(max_nesting > 0 && renderer); + + doc = hoedown_malloc(sizeof(hoedown_document)); + memcpy(&doc->md, renderer, sizeof(hoedown_renderer)); + + doc->data.opaque = renderer->opaque; + + hoedown_stack_init(&doc->work_bufs[BUFFER_BLOCK], 4); + hoedown_stack_init(&doc->work_bufs[BUFFER_SPAN], 8); + + memset(doc->active_char, 0x0, 256); + + if (extensions & HOEDOWN_EXT_UNDERLINE && doc->md.underline) { + doc->active_char['_'] = MD_CHAR_EMPHASIS; + } + + if (doc->md.emphasis || doc->md.double_emphasis || doc->md.triple_emphasis) { + doc->active_char['*'] = MD_CHAR_EMPHASIS; + doc->active_char['_'] = MD_CHAR_EMPHASIS; + if (extensions & HOEDOWN_EXT_STRIKETHROUGH) + doc->active_char['~'] = MD_CHAR_EMPHASIS; + if (extensions & HOEDOWN_EXT_HIGHLIGHT) + doc->active_char['='] = MD_CHAR_EMPHASIS; + } + + if (doc->md.codespan) + doc->active_char['`'] = MD_CHAR_CODESPAN; + + if (doc->md.linebreak) + doc->active_char['\n'] = MD_CHAR_LINEBREAK; + + if (doc->md.image || doc->md.link || doc->md.footnotes || doc->md.footnote_ref) + doc->active_char['['] = MD_CHAR_LINK; + + doc->active_char['<'] = MD_CHAR_LANGLE; + doc->active_char['\\'] = MD_CHAR_ESCAPE; + doc->active_char['&'] = MD_CHAR_ENTITY; + + if (extensions & HOEDOWN_EXT_AUTOLINK) { + doc->active_char[':'] = MD_CHAR_AUTOLINK_URL; + doc->active_char['@'] = MD_CHAR_AUTOLINK_EMAIL; + doc->active_char['w'] = MD_CHAR_AUTOLINK_WWW; + } + + if (extensions & HOEDOWN_EXT_SUPERSCRIPT) + doc->active_char['^'] = MD_CHAR_SUPERSCRIPT; + + if (extensions & HOEDOWN_EXT_QUOTE) + doc->active_char['"'] = MD_CHAR_QUOTE; + + if (extensions & HOEDOWN_EXT_MATH) + doc->active_char['$'] = MD_CHAR_MATH; + + /* Extension data */ + doc->ext_flags = extensions; + doc->max_nesting = max_nesting; + doc->in_link_body = 0; + + return doc; +} + +void +hoedown_document_render(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size) +{ + static const uint8_t UTF8_BOM[] = {0xEF, 0xBB, 0xBF}; + + hoedown_buffer *text; + size_t beg, end; + + int footnotes_enabled; + + text = hoedown_buffer_new(64); + + /* Preallocate enough space for our buffer to avoid expanding while copying */ + hoedown_buffer_grow(text, size); + + /* reset the references table */ + memset(&doc->refs, 0x0, REF_TABLE_SIZE * sizeof(void *)); + + footnotes_enabled = doc->ext_flags & HOEDOWN_EXT_FOOTNOTES; + + /* reset the footnotes lists */ + if (footnotes_enabled) { + memset(&doc->footnotes_found, 0x0, sizeof(doc->footnotes_found)); + memset(&doc->footnotes_used, 0x0, sizeof(doc->footnotes_used)); + } + + /* first pass: looking for references, copying everything else */ + beg = 0; + + /* Skip a possible UTF-8 BOM, even though the Unicode standard + * discourages having these in UTF-8 documents */ + if (size >= 3 && memcmp(data, UTF8_BOM, 3) == 0) + beg += 3; + + while (beg < size) /* iterating over lines */ + if (footnotes_enabled && is_footnote(data, beg, size, &end, &doc->footnotes_found)) + beg = end; + else if (is_ref(data, beg, size, &end, doc->refs)) + beg = end; + else { /* skipping to the next line */ + end = beg; + while (end < size && data[end] != '\n' && data[end] != '\r') + end++; + + /* adding the line body if present */ + if (end > beg) + expand_tabs(text, data + beg, end - beg); + + while (end < size && (data[end] == '\n' || data[end] == '\r')) { + /* add one \n per newline */ + if (data[end] == '\n' || (end + 1 < size && data[end + 1] != '\n')) + hoedown_buffer_putc(text, '\n'); + end++; + } + + beg = end; + } + + /* pre-grow the output buffer to minimize allocations */ + hoedown_buffer_grow(ob, text->size + (text->size >> 1)); + + /* second pass: actual rendering */ + if (doc->md.doc_header) + doc->md.doc_header(ob, 0, &doc->data); + + if (text->size) { + /* adding a final newline if not already present */ + if (text->data[text->size - 1] != '\n' && text->data[text->size - 1] != '\r') + hoedown_buffer_putc(text, '\n'); + + parse_block(ob, doc, text->data, text->size); + } + + /* footnotes */ + if (footnotes_enabled) + parse_footnote_list(ob, doc, &doc->footnotes_used); + + if (doc->md.doc_footer) + doc->md.doc_footer(ob, 0, &doc->data); + + /* clean-up */ + hoedown_buffer_free(text); + free_link_refs(doc->refs); + if (footnotes_enabled) { + free_footnote_list(&doc->footnotes_found, 1); + free_footnote_list(&doc->footnotes_used, 0); + } + + assert(doc->work_bufs[BUFFER_SPAN].size == 0); + assert(doc->work_bufs[BUFFER_BLOCK].size == 0); +} + +void +hoedown_document_render_inline(hoedown_document *doc, hoedown_buffer *ob, const uint8_t *data, size_t size) +{ + size_t i = 0, mark; + hoedown_buffer *text = hoedown_buffer_new(64); + + /* reset the references table */ + memset(&doc->refs, 0x0, REF_TABLE_SIZE * sizeof(void *)); + + /* first pass: expand tabs and process newlines */ + hoedown_buffer_grow(text, size); + while (1) { + mark = i; + while (i < size && data[i] != '\n' && data[i] != '\r') + i++; + + expand_tabs(text, data + mark, i - mark); + + if (i >= size) + break; + + while (i < size && (data[i] == '\n' || data[i] == '\r')) { + /* add one \n per newline */ + if (data[i] == '\n' || (i + 1 < size && data[i + 1] != '\n')) + hoedown_buffer_putc(text, '\n'); + i++; + } + } + + /* second pass: actual rendering */ + hoedown_buffer_grow(ob, text->size + (text->size >> 1)); + + if (doc->md.doc_header) + doc->md.doc_header(ob, 1, &doc->data); + + parse_inline(ob, doc, text->data, text->size); + + if (doc->md.doc_footer) + doc->md.doc_footer(ob, 1, &doc->data); + + /* clean-up */ + hoedown_buffer_free(text); + + assert(doc->work_bufs[BUFFER_SPAN].size == 0); + assert(doc->work_bufs[BUFFER_BLOCK].size == 0); +} + +void +hoedown_document_free(hoedown_document *doc) +{ + size_t i; + + for (i = 0; i < (size_t)doc->work_bufs[BUFFER_SPAN].asize; ++i) + hoedown_buffer_free(doc->work_bufs[BUFFER_SPAN].item[i]); + + for (i = 0; i < (size_t)doc->work_bufs[BUFFER_BLOCK].asize; ++i) + hoedown_buffer_free(doc->work_bufs[BUFFER_BLOCK].item[i]); + + hoedown_stack_uninit(&doc->work_bufs[BUFFER_SPAN]); + hoedown_stack_uninit(&doc->work_bufs[BUFFER_BLOCK]); + + free(doc); +} diff --git a/ultimmc/libraries/hoedown/src/escape.c b/ultimmc/libraries/hoedown/src/escape.c new file mode 100644 index 0000000..ce25dd5 --- /dev/null +++ b/ultimmc/libraries/hoedown/src/escape.c @@ -0,0 +1,188 @@ +#include "hoedown/escape.h" + +#include +#include +#include + + +#define likely(x) __builtin_expect((x),1) +#define unlikely(x) __builtin_expect((x),0) + + +/* + * The following characters will not be escaped: + * + * -_.+!*'(),%#@?=;:/,+&$ alphanum + * + * Note that this character set is the addition of: + * + * - The characters which are safe to be in an URL + * - The characters which are *not* safe to be in + * an URL because they are RESERVED characters. + * + * We assume (lazily) that any RESERVED char that + * appears inside an URL is actually meant to + * have its native function (i.e. as an URL + * component/separator) and hence needs no escaping. + * + * There are two exceptions: the chacters & (amp) + * and ' (single quote) do not appear in the table. + * They are meant to appear in the URL as components, + * yet they require special HTML-entity escaping + * to generate valid HTML markup. + * + * All other characters will be escaped to %XX. + * + */ +static const uint8_t HREF_SAFE[UINT8_MAX+1] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 1, + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +}; + +void +hoedown_escape_href(hoedown_buffer *ob, const uint8_t *data, size_t size) +{ + static const char hex_chars[] = "0123456789ABCDEF"; + size_t i = 0, mark; + char hex_str[3]; + + hex_str[0] = '%'; + + while (i < size) { + mark = i; + while (i < size && HREF_SAFE[data[i]]) i++; + + /* Optimization for cases where there's nothing to escape */ + if (mark == 0 && i >= size) { + hoedown_buffer_put(ob, data, size); + return; + } + + if (likely(i > mark)) { + hoedown_buffer_put(ob, data + mark, i - mark); + } + + /* escaping */ + if (i >= size) + break; + + switch (data[i]) { + /* amp appears all the time in URLs, but needs + * HTML-entity escaping to be inside an href */ + case '&': + HOEDOWN_BUFPUTSL(ob, "&"); + break; + + /* the single quote is a valid URL character + * according to the standard; it needs HTML + * entity escaping too */ + case '\'': + HOEDOWN_BUFPUTSL(ob, "'"); + break; + + /* the space can be escaped to %20 or a plus + * sign. we're going with the generic escape + * for now. the plus thing is more commonly seen + * when building GET strings */ +#if 0 + case ' ': + hoedown_buffer_putc(ob, '+'); + break; +#endif + + /* every other character goes with a %XX escaping */ + default: + hex_str[1] = hex_chars[(data[i] >> 4) & 0xF]; + hex_str[2] = hex_chars[data[i] & 0xF]; + hoedown_buffer_put(ob, (uint8_t *)hex_str, 3); + } + + i++; + } +} + + +/** + * According to the OWASP rules: + * + * & --> & + * < --> < + * > --> > + * " --> " + * ' --> ' ' is not recommended + * / --> / forward slash is included as it helps end an HTML entity + * + */ +static const uint8_t HTML_ESCAPE_TABLE[UINT8_MAX+1] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 4, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 6, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +}; + +static const char *HTML_ESCAPES[] = { + "", + """, + "&", + "'", + "/", + "<", + ">" +}; + +void +hoedown_escape_html(hoedown_buffer *ob, const uint8_t *data, size_t size, int secure) +{ + size_t i = 0, mark; + + while (1) { + mark = i; + while (i < size && HTML_ESCAPE_TABLE[data[i]] == 0) i++; + + /* Optimization for cases where there's nothing to escape */ + if (mark == 0 && i >= size) { + hoedown_buffer_put(ob, data, size); + return; + } + + if (likely(i > mark)) + hoedown_buffer_put(ob, data + mark, i - mark); + + if (i >= size) break; + + /* The forward slash is only escaped in secure mode */ + if (!secure && data[i] == '/') { + hoedown_buffer_putc(ob, '/'); + } else { + hoedown_buffer_puts(ob, HTML_ESCAPES[HTML_ESCAPE_TABLE[data[i]]]); + } + + i++; + } +} diff --git a/ultimmc/libraries/hoedown/src/html.c b/ultimmc/libraries/hoedown/src/html.c new file mode 100644 index 0000000..8bf3358 --- /dev/null +++ b/ultimmc/libraries/hoedown/src/html.c @@ -0,0 +1,754 @@ +#include "hoedown/html.h" + +#include +#include +#include +#include + +#include "hoedown/escape.h" + +#define USE_XHTML(opt) (opt->flags & HOEDOWN_HTML_USE_XHTML) + +hoedown_html_tag +hoedown_html_is_tag(const uint8_t *data, size_t size, const char *tagname) +{ + size_t i; + int closed = 0; + + if (size < 3 || data[0] != '<') + return HOEDOWN_HTML_TAG_NONE; + + i = 1; + + if (data[i] == '/') { + closed = 1; + i++; + } + + for (; i < size; ++i, ++tagname) { + if (*tagname == 0) + break; + + if (data[i] != *tagname) + return HOEDOWN_HTML_TAG_NONE; + } + + if (i == size) + return HOEDOWN_HTML_TAG_NONE; + + if (isspace(data[i]) || data[i] == '>') + return closed ? HOEDOWN_HTML_TAG_CLOSE : HOEDOWN_HTML_TAG_OPEN; + + return HOEDOWN_HTML_TAG_NONE; +} + +static void escape_html(hoedown_buffer *ob, const uint8_t *source, size_t length) +{ + hoedown_escape_html(ob, source, length, 0); +} + +static void escape_href(hoedown_buffer *ob, const uint8_t *source, size_t length) +{ + hoedown_escape_href(ob, source, length); +} + +/******************** + * GENERIC RENDERER * + ********************/ +static int +rndr_autolink(hoedown_buffer *ob, const hoedown_buffer *link, hoedown_autolink_type type, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + + if (!link || !link->size) + return 0; + + HOEDOWN_BUFPUTSL(ob, "data, link->size); + + if (state->link_attributes) { + hoedown_buffer_putc(ob, '\"'); + state->link_attributes(ob, link, data); + hoedown_buffer_putc(ob, '>'); + } else { + HOEDOWN_BUFPUTSL(ob, "\">"); + } + + /* + * Pretty printing: if we get an email address as + * an actual URI, e.g. `mailto:foo@bar.com`, we don't + * want to print the `mailto:` prefix + */ + if (hoedown_buffer_prefix(link, "mailto:") == 0) { + escape_html(ob, link->data + 7, link->size - 7); + } else { + escape_html(ob, link->data, link->size); + } + + HOEDOWN_BUFPUTSL(ob, ""); + + return 1; +} + +static void +rndr_blockcode(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_buffer *lang, const hoedown_renderer_data *data) +{ + if (ob->size) hoedown_buffer_putc(ob, '\n'); + + if (lang) { + HOEDOWN_BUFPUTSL(ob, "
    data, lang->size);
    +        HOEDOWN_BUFPUTSL(ob, "\">");
    +    } else {
    +        HOEDOWN_BUFPUTSL(ob, "
    ");
    +    }
    +
    +    if (text)
    +        escape_html(ob, text->data, text->size);
    +
    +    HOEDOWN_BUFPUTSL(ob, "
    \n"); +} + +static void +rndr_blockquote(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (ob->size) hoedown_buffer_putc(ob, '\n'); + HOEDOWN_BUFPUTSL(ob, "
    \n"); + if (content) hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, "
    \n"); +} + +static int +rndr_codespan(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data) +{ + HOEDOWN_BUFPUTSL(ob, ""); + if (text) escape_html(ob, text->data, text->size); + HOEDOWN_BUFPUTSL(ob, ""); + return 1; +} + +static int +rndr_strikethrough(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) + return 0; + + HOEDOWN_BUFPUTSL(ob, ""); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + return 1; +} + +static int +rndr_double_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) + return 0; + + HOEDOWN_BUFPUTSL(ob, ""); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + + return 1; +} + +static int +rndr_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) return 0; + HOEDOWN_BUFPUTSL(ob, ""); + if (content) hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + return 1; +} + +static int +rndr_underline(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) + return 0; + + HOEDOWN_BUFPUTSL(ob, ""); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + + return 1; +} + +static int +rndr_highlight(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) + return 0; + + HOEDOWN_BUFPUTSL(ob, ""); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + + return 1; +} + +static int +rndr_quote(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) + return 0; + + HOEDOWN_BUFPUTSL(ob, ""); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + + return 1; +} + +static int +rndr_linebreak(hoedown_buffer *ob, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + hoedown_buffer_puts(ob, USE_XHTML(state) ? "
    \n" : "
    \n"); + return 1; +} + +static void +rndr_header(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + + if (ob->size) + hoedown_buffer_putc(ob, '\n'); + + if (level <= state->toc_data.nesting_level) + hoedown_buffer_printf(ob, "", level, state->toc_data.header_count++); + else + hoedown_buffer_printf(ob, "", level); + + if (content) hoedown_buffer_put(ob, content->data, content->size); + hoedown_buffer_printf(ob, "\n", level); +} + +static int +rndr_link(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + + HOEDOWN_BUFPUTSL(ob, "size) + escape_href(ob, link->data, link->size); + + if (title && title->size) { + HOEDOWN_BUFPUTSL(ob, "\" title=\""); + escape_html(ob, title->data, title->size); + } + + if (state->link_attributes) { + hoedown_buffer_putc(ob, '\"'); + state->link_attributes(ob, link, data); + hoedown_buffer_putc(ob, '>'); + } else { + HOEDOWN_BUFPUTSL(ob, "\">"); + } + + if (content && content->size) hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + return 1; +} + +static void +rndr_list(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data) +{ + if (ob->size) hoedown_buffer_putc(ob, '\n'); + hoedown_buffer_put(ob, (const uint8_t *)(flags & HOEDOWN_LIST_ORDERED ? "
      \n" : "
        \n"), 5); + if (content) hoedown_buffer_put(ob, content->data, content->size); + hoedown_buffer_put(ob, (const uint8_t *)(flags & HOEDOWN_LIST_ORDERED ? "
    \n" : "\n"), 6); +} + +static void +rndr_listitem(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_list_flags flags, const hoedown_renderer_data *data) +{ + HOEDOWN_BUFPUTSL(ob, "
  • "); + if (content) { + size_t size = content->size; + while (size && content->data[size - 1] == '\n') + size--; + + hoedown_buffer_put(ob, content->data, size); + } + HOEDOWN_BUFPUTSL(ob, "
  • \n"); +} + +static void +rndr_paragraph(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + size_t i = 0; + + if (ob->size) hoedown_buffer_putc(ob, '\n'); + + if (!content || !content->size) + return; + + while (i < content->size && isspace(content->data[i])) i++; + + if (i == content->size) + return; + + HOEDOWN_BUFPUTSL(ob, "

    "); + if (state->flags & HOEDOWN_HTML_HARD_WRAP) { + size_t org; + while (i < content->size) { + org = i; + while (i < content->size && content->data[i] != '\n') + i++; + + if (i > org) + hoedown_buffer_put(ob, content->data + org, i - org); + + /* + * do not insert a line break if this newline + * is the last character on the paragraph + */ + if (i >= content->size - 1) + break; + + rndr_linebreak(ob, data); + i++; + } + } else { + hoedown_buffer_put(ob, content->data + i, content->size - i); + } + HOEDOWN_BUFPUTSL(ob, "

    \n"); +} + +static void +rndr_raw_block(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data) +{ + size_t org, sz; + + if (!text) + return; + + /* FIXME: Do we *really* need to trim the HTML? How does that make a difference? */ + sz = text->size; + while (sz > 0 && text->data[sz - 1] == '\n') + sz--; + + org = 0; + while (org < sz && text->data[org] == '\n') + org++; + + if (org >= sz) + return; + + if (ob->size) + hoedown_buffer_putc(ob, '\n'); + + hoedown_buffer_put(ob, text->data + org, sz - org); + hoedown_buffer_putc(ob, '\n'); +} + +static int +rndr_triple_emphasis(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) return 0; + HOEDOWN_BUFPUTSL(ob, ""); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + return 1; +} + +static void +rndr_hrule(hoedown_buffer *ob, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + if (ob->size) hoedown_buffer_putc(ob, '\n'); + hoedown_buffer_puts(ob, USE_XHTML(state) ? "
    \n" : "
    \n"); +} + +static int +rndr_image(hoedown_buffer *ob, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_buffer *alt, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + if (!link || !link->size) return 0; + + HOEDOWN_BUFPUTSL(ob, "data, link->size); + HOEDOWN_BUFPUTSL(ob, "\" alt=\""); + + if (alt && alt->size) + escape_html(ob, alt->data, alt->size); + + if (title && title->size) { + HOEDOWN_BUFPUTSL(ob, "\" title=\""); + escape_html(ob, title->data, title->size); } + + hoedown_buffer_puts(ob, USE_XHTML(state) ? "\"/>" : "\">"); + return 1; +} + +static int +rndr_raw_html(hoedown_buffer *ob, const hoedown_buffer *text, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + + /* ESCAPE overrides SKIP_HTML. It doesn't look to see if + * there are any valid tags, just escapes all of them. */ + if((state->flags & HOEDOWN_HTML_ESCAPE) != 0) { + escape_html(ob, text->data, text->size); + return 1; + } + + if ((state->flags & HOEDOWN_HTML_SKIP_HTML) != 0) + return 1; + + hoedown_buffer_put(ob, text->data, text->size); + return 1; +} + +static void +rndr_table(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (ob->size) hoedown_buffer_putc(ob, '\n'); + HOEDOWN_BUFPUTSL(ob, "\n"); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, "
    \n"); +} + +static void +rndr_table_header(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (ob->size) hoedown_buffer_putc(ob, '\n'); + HOEDOWN_BUFPUTSL(ob, "\n"); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, "\n"); +} + +static void +rndr_table_body(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (ob->size) hoedown_buffer_putc(ob, '\n'); + HOEDOWN_BUFPUTSL(ob, "\n"); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, "\n"); +} + +static void +rndr_tablerow(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + HOEDOWN_BUFPUTSL(ob, "\n"); + if (content) hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, "\n"); +} + +static void +rndr_tablecell(hoedown_buffer *ob, const hoedown_buffer *content, hoedown_table_flags flags, const hoedown_renderer_data *data) +{ + if (flags & HOEDOWN_TABLE_HEADER) { + HOEDOWN_BUFPUTSL(ob, ""); + break; + + case HOEDOWN_TABLE_ALIGN_LEFT: + HOEDOWN_BUFPUTSL(ob, " style=\"text-align: left\">"); + break; + + case HOEDOWN_TABLE_ALIGN_RIGHT: + HOEDOWN_BUFPUTSL(ob, " style=\"text-align: right\">"); + break; + + default: + HOEDOWN_BUFPUTSL(ob, ">"); + } + + if (content) + hoedown_buffer_put(ob, content->data, content->size); + + if (flags & HOEDOWN_TABLE_HEADER) { + HOEDOWN_BUFPUTSL(ob, "\n"); + } else { + HOEDOWN_BUFPUTSL(ob, "\n"); + } +} + +static int +rndr_superscript(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (!content || !content->size) return 0; + HOEDOWN_BUFPUTSL(ob, ""); + hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, ""); + return 1; +} + +static void +rndr_normal_text(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + if (content) + escape_html(ob, content->data, content->size); +} + +static void +rndr_footnotes(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + + if (ob->size) hoedown_buffer_putc(ob, '\n'); + HOEDOWN_BUFPUTSL(ob, "
    \n"); + hoedown_buffer_puts(ob, USE_XHTML(state) ? "
    \n" : "
    \n"); + HOEDOWN_BUFPUTSL(ob, "
      \n"); + + if (content) hoedown_buffer_put(ob, content->data, content->size); + + HOEDOWN_BUFPUTSL(ob, "\n
    \n
    \n"); +} + +static void +rndr_footnote_def(hoedown_buffer *ob, const hoedown_buffer *content, unsigned int num, const hoedown_renderer_data *data) +{ + size_t i = 0; + int pfound = 0; + + /* insert anchor at the end of first paragraph block */ + if (content) { + while ((i+3) < content->size) { + if (content->data[i++] != '<') continue; + if (content->data[i++] != '/') continue; + if (content->data[i++] != 'p' && content->data[i] != 'P') continue; + if (content->data[i] != '>') continue; + i -= 3; + pfound = 1; + break; + } + } + + hoedown_buffer_printf(ob, "\n
  • \n", num); + if (pfound) { + hoedown_buffer_put(ob, content->data, i); + hoedown_buffer_printf(ob, " ", num); + hoedown_buffer_put(ob, content->data + i, content->size - i); + } else if (content) { + hoedown_buffer_put(ob, content->data, content->size); + } + HOEDOWN_BUFPUTSL(ob, "
  • \n"); +} + +static int +rndr_footnote_ref(hoedown_buffer *ob, unsigned int num, const hoedown_renderer_data *data) +{ + hoedown_buffer_printf(ob, "%d", num, num, num); + return 1; +} + +static int +rndr_math(hoedown_buffer *ob, const hoedown_buffer *text, int displaymode, const hoedown_renderer_data *data) +{ + hoedown_buffer_put(ob, (const uint8_t *)(displaymode ? "\\[" : "\\("), 2); + escape_html(ob, text->data, text->size); + hoedown_buffer_put(ob, (const uint8_t *)(displaymode ? "\\]" : "\\)"), 2); + return 1; +} + +static void +toc_header(hoedown_buffer *ob, const hoedown_buffer *content, int level, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state = data->opaque; + + if (level <= state->toc_data.nesting_level) { + /* set the level offset if this is the first header + * we're parsing for the document */ + if (state->toc_data.current_level == 0) + state->toc_data.level_offset = level - 1; + + level -= state->toc_data.level_offset; + + if (level > state->toc_data.current_level) { + while (level > state->toc_data.current_level) { + HOEDOWN_BUFPUTSL(ob, "
      \n
    • \n"); + state->toc_data.current_level++; + } + } else if (level < state->toc_data.current_level) { + HOEDOWN_BUFPUTSL(ob, "
    • \n"); + while (level < state->toc_data.current_level) { + HOEDOWN_BUFPUTSL(ob, "
    \n
  • \n"); + state->toc_data.current_level--; + } + HOEDOWN_BUFPUTSL(ob,"
  • \n"); + } else { + HOEDOWN_BUFPUTSL(ob,"
  • \n
  • \n"); + } + + hoedown_buffer_printf(ob, "", state->toc_data.header_count++); + if (content) hoedown_buffer_put(ob, content->data, content->size); + HOEDOWN_BUFPUTSL(ob, "\n"); + } +} + +static int +toc_link(hoedown_buffer *ob, const hoedown_buffer *content, const hoedown_buffer *link, const hoedown_buffer *title, const hoedown_renderer_data *data) +{ + if (content && content->size) hoedown_buffer_put(ob, content->data, content->size); + return 1; +} + +static void +toc_finalize(hoedown_buffer *ob, int inline_render, const hoedown_renderer_data *data) +{ + hoedown_html_renderer_state *state; + + if (inline_render) + return; + + state = data->opaque; + + while (state->toc_data.current_level > 0) { + HOEDOWN_BUFPUTSL(ob, "
  • \n\n"); + state->toc_data.current_level--; + } + + state->toc_data.header_count = 0; +} + +hoedown_renderer * +hoedown_html_toc_renderer_new(int nesting_level) +{ + static const hoedown_renderer cb_default = { + NULL, + + NULL, + NULL, + toc_header, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + + NULL, + rndr_codespan, + rndr_double_emphasis, + rndr_emphasis, + rndr_underline, + rndr_highlight, + rndr_quote, + NULL, + NULL, + toc_link, + rndr_triple_emphasis, + rndr_strikethrough, + rndr_superscript, + NULL, + NULL, + NULL, + + NULL, + rndr_normal_text, + + NULL, + toc_finalize + }; + + hoedown_html_renderer_state *state; + hoedown_renderer *renderer; + + /* Prepare the state pointer */ + state = hoedown_malloc(sizeof(hoedown_html_renderer_state)); + memset(state, 0x0, sizeof(hoedown_html_renderer_state)); + + state->toc_data.nesting_level = nesting_level; + + /* Prepare the renderer */ + renderer = hoedown_malloc(sizeof(hoedown_renderer)); + memcpy(renderer, &cb_default, sizeof(hoedown_renderer)); + + renderer->opaque = state; + return renderer; +} + +hoedown_renderer * +hoedown_html_renderer_new(hoedown_html_flags render_flags, int nesting_level) +{ + static const hoedown_renderer cb_default = { + NULL, + + rndr_blockcode, + rndr_blockquote, + rndr_header, + rndr_hrule, + rndr_list, + rndr_listitem, + rndr_paragraph, + rndr_table, + rndr_table_header, + rndr_table_body, + rndr_tablerow, + rndr_tablecell, + rndr_footnotes, + rndr_footnote_def, + rndr_raw_block, + + rndr_autolink, + rndr_codespan, + rndr_double_emphasis, + rndr_emphasis, + rndr_underline, + rndr_highlight, + rndr_quote, + rndr_image, + rndr_linebreak, + rndr_link, + rndr_triple_emphasis, + rndr_strikethrough, + rndr_superscript, + rndr_footnote_ref, + rndr_math, + rndr_raw_html, + + NULL, + rndr_normal_text, + + NULL, + NULL + }; + + hoedown_html_renderer_state *state; + hoedown_renderer *renderer; + + /* Prepare the state pointer */ + state = hoedown_malloc(sizeof(hoedown_html_renderer_state)); + memset(state, 0x0, sizeof(hoedown_html_renderer_state)); + + state->flags = render_flags; + state->toc_data.nesting_level = nesting_level; + + /* Prepare the renderer */ + renderer = hoedown_malloc(sizeof(hoedown_renderer)); + memcpy(renderer, &cb_default, sizeof(hoedown_renderer)); + + if (render_flags & HOEDOWN_HTML_SKIP_HTML || render_flags & HOEDOWN_HTML_ESCAPE) + renderer->blockhtml = NULL; + + renderer->opaque = state; + return renderer; +} + +void +hoedown_html_renderer_free(hoedown_renderer *renderer) +{ + free(renderer->opaque); + free(renderer); +} diff --git a/ultimmc/libraries/hoedown/src/html_blocks.c b/ultimmc/libraries/hoedown/src/html_blocks.c new file mode 100644 index 0000000..f5e9dce --- /dev/null +++ b/ultimmc/libraries/hoedown/src/html_blocks.c @@ -0,0 +1,240 @@ +/* ANSI-C code produced by gperf version 3.0.3 */ +/* Command-line: gperf -L ANSI-C -N hoedown_find_block_tag -c -C -E -S 1 --ignore-case -m100 html_block_names.gperf */ +/* Computed positions: -k'1-2' */ + +#if !((' ' == 32) && ('!' == 33) && ('"' == 34) && ('#' == 35) \ + && ('%' == 37) && ('&' == 38) && ('\'' == 39) && ('(' == 40) \ + && (')' == 41) && ('*' == 42) && ('+' == 43) && (',' == 44) \ + && ('-' == 45) && ('.' == 46) && ('/' == 47) && ('0' == 48) \ + && ('1' == 49) && ('2' == 50) && ('3' == 51) && ('4' == 52) \ + && ('5' == 53) && ('6' == 54) && ('7' == 55) && ('8' == 56) \ + && ('9' == 57) && (':' == 58) && (';' == 59) && ('<' == 60) \ + && ('=' == 61) && ('>' == 62) && ('?' == 63) && ('A' == 65) \ + && ('B' == 66) && ('C' == 67) && ('D' == 68) && ('E' == 69) \ + && ('F' == 70) && ('G' == 71) && ('H' == 72) && ('I' == 73) \ + && ('J' == 74) && ('K' == 75) && ('L' == 76) && ('M' == 77) \ + && ('N' == 78) && ('O' == 79) && ('P' == 80) && ('Q' == 81) \ + && ('R' == 82) && ('S' == 83) && ('T' == 84) && ('U' == 85) \ + && ('V' == 86) && ('W' == 87) && ('X' == 88) && ('Y' == 89) \ + && ('Z' == 90) && ('[' == 91) && ('\\' == 92) && (']' == 93) \ + && ('^' == 94) && ('_' == 95) && ('a' == 97) && ('b' == 98) \ + && ('c' == 99) && ('d' == 100) && ('e' == 101) && ('f' == 102) \ + && ('g' == 103) && ('h' == 104) && ('i' == 105) && ('j' == 106) \ + && ('k' == 107) && ('l' == 108) && ('m' == 109) && ('n' == 110) \ + && ('o' == 111) && ('p' == 112) && ('q' == 113) && ('r' == 114) \ + && ('s' == 115) && ('t' == 116) && ('u' == 117) && ('v' == 118) \ + && ('w' == 119) && ('x' == 120) && ('y' == 121) && ('z' == 122) \ + && ('{' == 123) && ('|' == 124) && ('}' == 125) && ('~' == 126)) +/* The character set is not based on ISO-646. */ +#error "gperf generated tables don't work with this execution character set. Please report a bug to ." +#endif + +/* maximum key range = 24, duplicates = 0 */ + +#ifndef GPERF_DOWNCASE +#define GPERF_DOWNCASE 1 +static unsigned char gperf_downcase[256] = + { + 0, 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, 62, 63, 64, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, + 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, + 122, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, + 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, + 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, + 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, + 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, + 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, + 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, + 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, + 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, + 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, + 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, + 255 + }; +#endif + +#ifndef GPERF_CASE_STRNCMP +#define GPERF_CASE_STRNCMP 1 +static int +gperf_case_strncmp (register const char *s1, register const char *s2, register unsigned int n) +{ + for (; n > 0;) + { + unsigned char c1 = gperf_downcase[(unsigned char)*s1++]; + unsigned char c2 = gperf_downcase[(unsigned char)*s2++]; + if (c1 != 0 && c1 == c2) + { + n--; + continue; + } + return (int)c1 - (int)c2; + } + return 0; +} +#endif + +#ifdef __GNUC__ +__inline +#else +#ifdef __cplusplus +inline +#endif +#endif +static unsigned int +hash (register const char *str, register unsigned int len) +{ + static const unsigned char asso_values[] = + { + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 22, 21, 19, 18, 16, 0, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 1, 25, 0, 25, + 1, 0, 0, 13, 0, 25, 25, 11, 2, 1, + 0, 25, 25, 5, 0, 2, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 1, 25, + 0, 25, 1, 0, 0, 13, 0, 25, 25, 11, + 2, 1, 0, 25, 25, 5, 0, 2, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, + 25, 25, 25, 25, 25, 25, 25 + }; + register int hval = (int)len; + + switch (hval) + { + default: + hval += asso_values[(unsigned char)str[1]+1]; + /*FALLTHROUGH*/ + case 1: + hval += asso_values[(unsigned char)str[0]]; + break; + } + return hval; +} + +#ifdef __GNUC__ +__inline +#ifdef __GNUC_STDC_INLINE__ +__attribute__ ((__gnu_inline__)) +#endif +#endif +const char * +hoedown_find_block_tag (register const char *str, register unsigned int len) +{ + enum + { + TOTAL_KEYWORDS = 24, + MIN_WORD_LENGTH = 1, + MAX_WORD_LENGTH = 10, + MIN_HASH_VALUE = 1, + MAX_HASH_VALUE = 24 + }; + + if (len <= MAX_WORD_LENGTH && len >= MIN_WORD_LENGTH) + { + register int key = hash (str, len); + + if (key <= MAX_HASH_VALUE && key >= MIN_HASH_VALUE) + { + register const char *resword; + + switch (key - 1) + { + case 0: + resword = "p"; + goto compare; + case 1: + resword = "h6"; + goto compare; + case 2: + resword = "div"; + goto compare; + case 3: + resword = "del"; + goto compare; + case 4: + resword = "form"; + goto compare; + case 5: + resword = "table"; + goto compare; + case 6: + resword = "figure"; + goto compare; + case 7: + resword = "pre"; + goto compare; + case 8: + resword = "fieldset"; + goto compare; + case 9: + resword = "noscript"; + goto compare; + case 10: + resword = "script"; + goto compare; + case 11: + resword = "style"; + goto compare; + case 12: + resword = "dl"; + goto compare; + case 13: + resword = "ol"; + goto compare; + case 14: + resword = "ul"; + goto compare; + case 15: + resword = "math"; + goto compare; + case 16: + resword = "ins"; + goto compare; + case 17: + resword = "h5"; + goto compare; + case 18: + resword = "iframe"; + goto compare; + case 19: + resword = "h4"; + goto compare; + case 20: + resword = "h3"; + goto compare; + case 21: + resword = "blockquote"; + goto compare; + case 22: + resword = "h2"; + goto compare; + case 23: + resword = "h1"; + goto compare; + } + return 0; + compare: + if ((((unsigned char)*str ^ (unsigned char)*resword) & ~32) == 0 && !gperf_case_strncmp (str, resword, len) && resword[len] == '\0') + return resword; + } + } + return 0; +} diff --git a/ultimmc/libraries/hoedown/src/html_smartypants.c b/ultimmc/libraries/hoedown/src/html_smartypants.c new file mode 100644 index 0000000..d89624f --- /dev/null +++ b/ultimmc/libraries/hoedown/src/html_smartypants.c @@ -0,0 +1,435 @@ +#include "hoedown/html.h" + +#include +#include +#include +#include + +#ifdef _MSC_VER +#define snprintf _snprintf +#endif + +struct smartypants_data { + int in_squote; + int in_dquote; +}; + +static size_t smartypants_cb__ltag(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__dquote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__amp(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__period(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__number(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__dash(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__parens(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__backtick(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); +static size_t smartypants_cb__escape(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size); + +static size_t (*smartypants_cb_ptrs[]) + (hoedown_buffer *, struct smartypants_data *, uint8_t, const uint8_t *, size_t) = +{ + NULL, /* 0 */ + smartypants_cb__dash, /* 1 */ + smartypants_cb__parens, /* 2 */ + smartypants_cb__squote, /* 3 */ + smartypants_cb__dquote, /* 4 */ + smartypants_cb__amp, /* 5 */ + smartypants_cb__period, /* 6 */ + smartypants_cb__number, /* 7 */ + smartypants_cb__ltag, /* 8 */ + smartypants_cb__backtick, /* 9 */ + smartypants_cb__escape, /* 10 */ +}; + +static const uint8_t smartypants_cb_chars[UINT8_MAX+1] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 4, 0, 0, 0, 5, 3, 2, 0, 0, 0, 0, 1, 6, 0, + 0, 7, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, + 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +}; + +static int +word_boundary(uint8_t c) +{ + return c == 0 || isspace(c) || ispunct(c); +} + +/* + If 'text' begins with any kind of single quote (e.g. "'" or "'" etc.), + returns the length of the sequence of characters that makes up the single- + quote. Otherwise, returns zero. +*/ +static size_t +squote_len(const uint8_t *text, size_t size) +{ + static char* single_quote_list[] = { "'", "'", "'", "'", NULL }; + char** p; + + for (p = single_quote_list; *p; ++p) { + size_t len = strlen(*p); + if (size >= len && memcmp(text, *p, len) == 0) { + return len; + } + } + + return 0; +} + +/* Converts " or ' at very beginning or end of a word to left or right quote */ +static int +smartypants_quotes(hoedown_buffer *ob, uint8_t previous_char, uint8_t next_char, uint8_t quote, int *is_open) +{ + char ent[8]; + + if (*is_open && !word_boundary(next_char)) + return 0; + + if (!(*is_open) && !word_boundary(previous_char)) + return 0; + + snprintf(ent, sizeof(ent), "&%c%cquo;", (*is_open) ? 'r' : 'l', quote); + *is_open = !(*is_open); + hoedown_buffer_puts(ob, ent); + return 1; +} + +/* + Converts ' to left or right single quote; but the initial ' might be in + different forms, e.g. ' or ' or '. + 'squote_text' points to the original single quote, and 'squote_size' is its length. + 'text' points at the last character of the single-quote, e.g. ' or ; +*/ +static size_t +smartypants_squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size, + const uint8_t *squote_text, size_t squote_size) +{ + if (size >= 2) { + uint8_t t1 = tolower(text[1]); + size_t next_squote_len = squote_len(text+1, size-1); + + /* convert '' to “ or ” */ + if (next_squote_len > 0) { + uint8_t next_char = (size > 1+next_squote_len) ? text[1+next_squote_len] : 0; + if (smartypants_quotes(ob, previous_char, next_char, 'd', &smrt->in_dquote)) + return next_squote_len; + } + + /* Tom's, isn't, I'm, I'd */ + if ((t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && + (size == 3 || word_boundary(text[2]))) { + HOEDOWN_BUFPUTSL(ob, "’"); + return 0; + } + + /* you're, you'll, you've */ + if (size >= 3) { + uint8_t t2 = tolower(text[2]); + + if (((t1 == 'r' && t2 == 'e') || + (t1 == 'l' && t2 == 'l') || + (t1 == 'v' && t2 == 'e')) && + (size == 4 || word_boundary(text[3]))) { + HOEDOWN_BUFPUTSL(ob, "’"); + return 0; + } + } + } + + if (smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 's', &smrt->in_squote)) + return 0; + + hoedown_buffer_put(ob, squote_text, squote_size); + return 0; +} + +/* Converts ' to left or right single quote. */ +static size_t +smartypants_cb__squote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + return smartypants_squote(ob, smrt, previous_char, text, size, text, 1); +} + +/* Converts (c), (r), (tm) */ +static size_t +smartypants_cb__parens(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + if (size >= 3) { + uint8_t t1 = tolower(text[1]); + uint8_t t2 = tolower(text[2]); + + if (t1 == 'c' && t2 == ')') { + HOEDOWN_BUFPUTSL(ob, "©"); + return 2; + } + + if (t1 == 'r' && t2 == ')') { + HOEDOWN_BUFPUTSL(ob, "®"); + return 2; + } + + if (size >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')') { + HOEDOWN_BUFPUTSL(ob, "™"); + return 3; + } + } + + hoedown_buffer_putc(ob, text[0]); + return 0; +} + +/* Converts "--" to em-dash, etc. */ +static size_t +smartypants_cb__dash(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + if (size >= 3 && text[1] == '-' && text[2] == '-') { + HOEDOWN_BUFPUTSL(ob, "—"); + return 2; + } + + if (size >= 2 && text[1] == '-') { + HOEDOWN_BUFPUTSL(ob, "–"); + return 1; + } + + hoedown_buffer_putc(ob, text[0]); + return 0; +} + +/* Converts " etc. */ +static size_t +smartypants_cb__amp(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + size_t len; + if (size >= 6 && memcmp(text, """, 6) == 0) { + if (smartypants_quotes(ob, previous_char, size >= 7 ? text[6] : 0, 'd', &smrt->in_dquote)) + return 5; + } + + len = squote_len(text, size); + if (len > 0) { + return (len-1) + smartypants_squote(ob, smrt, previous_char, text+(len-1), size-(len-1), text, len); + } + + if (size >= 4 && memcmp(text, "�", 4) == 0) + return 3; + + hoedown_buffer_putc(ob, '&'); + return 0; +} + +/* Converts "..." to ellipsis */ +static size_t +smartypants_cb__period(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + if (size >= 3 && text[1] == '.' && text[2] == '.') { + HOEDOWN_BUFPUTSL(ob, "…"); + return 2; + } + + if (size >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.') { + HOEDOWN_BUFPUTSL(ob, "…"); + return 4; + } + + hoedown_buffer_putc(ob, text[0]); + return 0; +} + +/* Converts `` to opening double quote */ +static size_t +smartypants_cb__backtick(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + if (size >= 2 && text[1] == '`') { + if (smartypants_quotes(ob, previous_char, size >= 3 ? text[2] : 0, 'd', &smrt->in_dquote)) + return 1; + } + + hoedown_buffer_putc(ob, text[0]); + return 0; +} + +/* Converts 1/2, 1/4, 3/4 */ +static size_t +smartypants_cb__number(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + if (word_boundary(previous_char) && size >= 3) { + if (text[0] == '1' && text[1] == '/' && text[2] == '2') { + if (size == 3 || word_boundary(text[3])) { + HOEDOWN_BUFPUTSL(ob, "½"); + return 2; + } + } + + if (text[0] == '1' && text[1] == '/' && text[2] == '4') { + if (size == 3 || word_boundary(text[3]) || + (size >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h')) { + HOEDOWN_BUFPUTSL(ob, "¼"); + return 2; + } + } + + if (text[0] == '3' && text[1] == '/' && text[2] == '4') { + if (size == 3 || word_boundary(text[3]) || + (size >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's')) { + HOEDOWN_BUFPUTSL(ob, "¾"); + return 2; + } + } + } + + hoedown_buffer_putc(ob, text[0]); + return 0; +} + +/* Converts " to left or right double quote */ +static size_t +smartypants_cb__dquote(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + if (!smartypants_quotes(ob, previous_char, size > 0 ? text[1] : 0, 'd', &smrt->in_dquote)) + HOEDOWN_BUFPUTSL(ob, """); + + return 0; +} + +static size_t +smartypants_cb__ltag(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + static const char *skip_tags[] = { + "pre", "code", "var", "samp", "kbd", "math", "script", "style" + }; + static const size_t skip_tags_count = 8; + + size_t tag, i = 0; + + /* This is a comment. Copy everything verbatim until --> or EOF is seen. */ + if (i + 4 < size && memcmp(text, "", 3) != 0) + i++; + i += 3; + hoedown_buffer_put(ob, text, i + 1); + return i; + } + + while (i < size && text[i] != '>') + i++; + + for (tag = 0; tag < skip_tags_count; ++tag) { + if (hoedown_html_is_tag(text, size, skip_tags[tag]) == HOEDOWN_HTML_TAG_OPEN) + break; + } + + if (tag < skip_tags_count) { + for (;;) { + while (i < size && text[i] != '<') + i++; + + if (i == size) + break; + + if (hoedown_html_is_tag(text + i, size - i, skip_tags[tag]) == HOEDOWN_HTML_TAG_CLOSE) + break; + + i++; + } + + while (i < size && text[i] != '>') + i++; + } + + hoedown_buffer_put(ob, text, i + 1); + return i; +} + +static size_t +smartypants_cb__escape(hoedown_buffer *ob, struct smartypants_data *smrt, uint8_t previous_char, const uint8_t *text, size_t size) +{ + if (size < 2) + return 0; + + switch (text[1]) { + case '\\': + case '"': + case '\'': + case '.': + case '-': + case '`': + hoedown_buffer_putc(ob, text[1]); + return 1; + + default: + hoedown_buffer_putc(ob, '\\'); + return 0; + } +} + +#if 0 +static struct { + uint8_t c0; + const uint8_t *pattern; + const uint8_t *entity; + int skip; +} smartypants_subs[] = { + { '\'', "'s>", "’", 0 }, + { '\'', "'t>", "’", 0 }, + { '\'', "'re>", "’", 0 }, + { '\'', "'ll>", "’", 0 }, + { '\'', "'ve>", "’", 0 }, + { '\'', "'m>", "’", 0 }, + { '\'', "'d>", "’", 0 }, + { '-', "--", "—", 1 }, + { '-', "<->", "–", 0 }, + { '.', "...", "…", 2 }, + { '.', ". . .", "…", 4 }, + { '(', "(c)", "©", 2 }, + { '(', "(r)", "®", 2 }, + { '(', "(tm)", "™", 3 }, + { '3', "<3/4>", "¾", 2 }, + { '3', "<3/4ths>", "¾", 2 }, + { '1', "<1/2>", "½", 2 }, + { '1', "<1/4>", "¼", 2 }, + { '1', "<1/4th>", "¼", 2 }, + { '&', "�", 0, 3 }, +}; +#endif + +void +hoedown_html_smartypants(hoedown_buffer *ob, const uint8_t *text, size_t size) +{ + size_t i; + struct smartypants_data smrt = {0, 0}; + + if (!text) + return; + + hoedown_buffer_grow(ob, size); + + for (i = 0; i < size; ++i) { + size_t org; + uint8_t action = 0; + + org = i; + while (i < size && (action = smartypants_cb_chars[text[i]]) == 0) + i++; + + if (i > org) + hoedown_buffer_put(ob, text + org, i - org); + + if (i < size) { + i += smartypants_cb_ptrs[(int)action] + (ob, &smrt, i ? text[i - 1] : 0, text + i, size - i); + } + } +} diff --git a/ultimmc/libraries/hoedown/src/stack.c b/ultimmc/libraries/hoedown/src/stack.c new file mode 100644 index 0000000..0523c11 --- /dev/null +++ b/ultimmc/libraries/hoedown/src/stack.c @@ -0,0 +1,79 @@ +#include "hoedown/stack.h" + +#include "hoedown/buffer.h" + +#include +#include +#include + +void +hoedown_stack_init(hoedown_stack *st, size_t initial_size) +{ + assert(st); + + st->item = NULL; + st->size = st->asize = 0; + + if (!initial_size) + initial_size = 8; + + hoedown_stack_grow(st, initial_size); +} + +void +hoedown_stack_uninit(hoedown_stack *st) +{ + assert(st); + + free(st->item); +} + +void +hoedown_stack_grow(hoedown_stack *st, size_t neosz) +{ + assert(st); + + if (st->asize >= neosz) + return; + + st->item = hoedown_realloc(st->item, neosz * sizeof(void *)); + memset(st->item + st->asize, 0x0, (neosz - st->asize) * sizeof(void *)); + + st->asize = neosz; + + if (st->size > neosz) + st->size = neosz; +} + +void +hoedown_stack_push(hoedown_stack *st, void *item) +{ + assert(st); + + if (st->size >= st->asize) + hoedown_stack_grow(st, st->size * 2); + + st->item[st->size++] = item; +} + +void * +hoedown_stack_pop(hoedown_stack *st) +{ + assert(st); + + if (!st->size) + return NULL; + + return st->item[--st->size]; +} + +void * +hoedown_stack_top(const hoedown_stack *st) +{ + assert(st); + + if (!st->size) + return NULL; + + return st->item[st->size - 1]; +} diff --git a/ultimmc/libraries/hoedown/src/version.c b/ultimmc/libraries/hoedown/src/version.c new file mode 100644 index 0000000..10d36cb --- /dev/null +++ b/ultimmc/libraries/hoedown/src/version.c @@ -0,0 +1,9 @@ +#include "hoedown/version.h" + +void +hoedown_version(int *major, int *minor, int *revision) +{ + *major = HOEDOWN_VERSION_MAJOR; + *minor = HOEDOWN_VERSION_MINOR; + *revision = HOEDOWN_VERSION_REVISION; +} diff --git a/ultimmc/libraries/iconfix/CMakeLists.txt b/ultimmc/libraries/iconfix/CMakeLists.txt new file mode 100644 index 0000000..52a31c6 --- /dev/null +++ b/ultimmc/libraries/iconfix/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.1) +project(iconfix) + +find_package(Qt5Core REQUIRED QUIET) +find_package(Qt5Widgets REQUIRED QUIET) + +set(ICONFIX_SOURCES +xdgicon.h +xdgicon.cpp +internal/qhexstring_p.h +internal/qiconloader.cpp +internal/qiconloader_p.h +) + +add_library(Launcher_iconfix SHARED ${ICONFIX_SOURCES}) +target_include_directories(Launcher_iconfix PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} "${CMAKE_CURRENT_BINARY_DIR}" ) + +target_link_libraries(Launcher_iconfix Qt5::Core Qt5::Widgets) + +set_target_properties(Launcher_iconfix PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN 1) +generate_export_header(Launcher_iconfix) + +# Install it +install( + TARGETS Launcher_iconfix + RUNTIME DESTINATION ${LIBRARY_DEST_DIR} + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} +) \ No newline at end of file diff --git a/ultimmc/libraries/iconfix/internal/qhexstring_p.h b/ultimmc/libraries/iconfix/internal/qhexstring_p.h new file mode 100644 index 0000000..c81904e --- /dev/null +++ b/ultimmc/libraries/iconfix/internal/qhexstring_p.h @@ -0,0 +1,100 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include +#include +#include +#include +#include + +#pragma once + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +// internal helper. Converts an integer value to an unique string token +template struct HexString +{ + inline HexString(const T t) : val(t) + { + } + + inline void write(QChar *&dest) const + { + const ushort hexChars[] = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + const char *c = reinterpret_cast(&val); + for (uint i = 0; i < sizeof(T); ++i) + { + *dest++ = hexChars[*c & 0xf]; + *dest++ = hexChars[(*c & 0xf0) >> 4]; + ++c; + } + } + const T val; +}; + +// specialization to enable fast concatenating of our string tokens to a string +template struct QConcatenable> +{ + typedef HexString type; + enum + { + ExactSize = true + }; + static int size(const HexString &) + { + return sizeof(T) * 2; + } + static inline void appendTo(const HexString &str, QChar *&out) + { + str.write(out); + } + typedef QString ConvertTo; +}; diff --git a/ultimmc/libraries/iconfix/internal/qiconloader.cpp b/ultimmc/libraries/iconfix/internal/qiconloader.cpp new file mode 100644 index 0000000..0d8466f --- /dev/null +++ b/ultimmc/libraries/iconfix/internal/qiconloader.cpp @@ -0,0 +1,688 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +#include "qiconloader_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "qhexstring_p.h" + +namespace QtXdg +{ + +Q_GLOBAL_STATIC(QIconLoader, iconLoaderInstance) + +/* Theme to use in last resort, if the theme does not have the icon, neither the parents */ + +static QString fallbackTheme() +{ + return QString("hicolor"); +} + +QIconLoader::QIconLoader() : m_themeKey(1), m_supportsSvg(false), m_initialized(false) +{ +} + +// We lazily initialize the loader to make static icons +// work. Though we do not officially support this. + +static inline QString systemThemeName() +{ + return QIcon::themeName(); +} + +static inline QStringList systemIconSearchPaths() +{ + auto paths = QIcon::themeSearchPaths(); + paths.push_front(":/icons"); + return paths; +} + +void QIconLoader::ensureInitialized() +{ + if (!m_initialized) + { + m_initialized = true; + + Q_ASSERT(qApp); + + m_systemTheme = QIcon::themeName(); + + if (m_systemTheme.isEmpty()) + m_systemTheme = fallbackTheme(); + m_supportsSvg = true; + } +} + +QIconLoader *QIconLoader::instance() +{ + iconLoaderInstance()->ensureInitialized(); + return iconLoaderInstance(); +} + +// Queries the system theme and invalidates existing +// icons if the theme has changed. +void QIconLoader::updateSystemTheme() +{ + // Only change if this is not explicitly set by the user + if (m_userTheme.isEmpty()) + { + QString theme = systemThemeName(); + if (theme.isEmpty()) + theme = fallbackTheme(); + if (theme != m_systemTheme) + { + m_systemTheme = theme; + invalidateKey(); + } + } +} + +void QIconLoader::setThemeName(const QString &themeName) +{ + m_userTheme = themeName; + invalidateKey(); +} + +void QIconLoader::setThemeSearchPath(const QStringList &searchPaths) +{ + m_iconDirs = searchPaths; + themeList.clear(); + invalidateKey(); +} + +QStringList QIconLoader::themeSearchPaths() const +{ + if (m_iconDirs.isEmpty()) + { + m_iconDirs = systemIconSearchPaths(); + } + return m_iconDirs; +} + +QIconTheme::QIconTheme(const QString &themeName) : m_valid(false) +{ + QFile themeIndex; + + QStringList iconDirs = systemIconSearchPaths(); + for (int i = 0; i < iconDirs.size(); ++i) + { + QDir iconDir(iconDirs[i]); + QString themeDir = iconDir.path() + QLatin1Char('/') + themeName; + themeIndex.setFileName(themeDir + QLatin1String("/index.theme")); + if (themeIndex.exists()) + { + m_contentDir = themeDir; + m_valid = true; + + foreach (QString path, iconDirs) + { + if (QFileInfo(path).isDir()) + m_contentDirs.append(path + QLatin1Char('/') + themeName); + } + + break; + } + } + + // if there is no index file, abscond. + if (!themeIndex.exists()) + return; + + // otherwise continue reading index file + const QSettings indexReader(themeIndex.fileName(), QSettings::IniFormat); + QStringListIterator keyIterator(indexReader.allKeys()); + while (keyIterator.hasNext()) + { + const QString key = keyIterator.next(); + if (!key.endsWith(QLatin1String("/Size"))) + continue; + + // Note the QSettings ini-format does not accept + // slashes in key names, hence we have to cheat + int size = indexReader.value(key).toInt(); + if (!size) + continue; + + QString directoryKey = key.left(key.size() - 5); + QIconDirInfo dirInfo(directoryKey); + dirInfo.size = size; + QString type = + indexReader.value(directoryKey + QLatin1String("/Type")).toString(); + + if (type == QLatin1String("Fixed")) + dirInfo.type = QIconDirInfo::Fixed; + else if (type == QLatin1String("Scalable")) + dirInfo.type = QIconDirInfo::Scalable; + else + dirInfo.type = QIconDirInfo::Threshold; + + dirInfo.threshold = + indexReader.value(directoryKey + QLatin1String("/Threshold"), 2) + .toInt(); + + dirInfo.minSize = + indexReader.value(directoryKey + QLatin1String("/MinSize"), size) + .toInt(); + + dirInfo.maxSize = + indexReader.value(directoryKey + QLatin1String("/MaxSize"), size) + .toInt(); + m_keyList.append(dirInfo); + } + + // Parent themes provide fallbacks for missing icons + m_parents = indexReader.value(QLatin1String("Icon Theme/Inherits")).toStringList(); + m_parents.removeAll(QString()); + + // Ensure a default platform fallback for all themes + if (m_parents.isEmpty()) + { + const QString fallback = fallbackTheme(); + if (!fallback.isEmpty()) + m_parents.append(fallback); + } + + // Ensure that all themes fall back to hicolor + if (!m_parents.contains(QLatin1String("hicolor"))) + m_parents.append(QLatin1String("hicolor")); +} + +QThemeIconEntries QIconLoader::findIconHelper(const QString &themeName, const QString &iconName, + QStringList &visited) const +{ + QThemeIconEntries entries; + Q_ASSERT(!themeName.isEmpty()); + + QPixmap pixmap; + + // Used to protect against potential recursions + visited << themeName; + + QIconTheme theme = themeList.value(themeName); + if (!theme.isValid()) + { + theme = QIconTheme(themeName); + if (!theme.isValid()) + theme = QIconTheme(fallbackTheme()); + + themeList.insert(themeName, theme); + } + + QStringList contentDirs = theme.contentDirs(); + const QVector subDirs = theme.keyList(); + + const QString svgext(QLatin1String(".svg")); + const QString pngext(QLatin1String(".png")); + const QString xpmext(QLatin1String(".xpm")); + + // Add all relevant files + for (int i = 0; i < subDirs.size(); ++i) + { + const QIconDirInfo &dirInfo = subDirs.at(i); + QString subdir = dirInfo.path; + + foreach (QString contentDir, contentDirs) + { + QDir currentDir(contentDir + '/' + subdir); + + if (currentDir.exists(iconName + pngext)) + { + PixmapEntry *iconEntry = new PixmapEntry; + iconEntry->dir = dirInfo; + iconEntry->filename = currentDir.filePath(iconName + pngext); + // Notice we ensure that pixmap entries always come before + // scalable to preserve search order afterwards + entries.prepend(iconEntry); + } + else if (m_supportsSvg && currentDir.exists(iconName + svgext)) + { + ScalableEntry *iconEntry = new ScalableEntry; + iconEntry->dir = dirInfo; + iconEntry->filename = currentDir.filePath(iconName + svgext); + entries.append(iconEntry); + break; + } + else if (currentDir.exists(iconName + xpmext)) + { + PixmapEntry *iconEntry = new PixmapEntry; + iconEntry->dir = dirInfo; + iconEntry->filename = currentDir.filePath(iconName + xpmext); + // Notice we ensure that pixmap entries always come before + // scalable to preserve search order afterwards + entries.append(iconEntry); + break; + } + } + } + + if (entries.isEmpty()) + { + const QStringList parents = theme.parents(); + // Search recursively through inherited themes + for (int i = 0; i < parents.size(); ++i) + { + + const QString parentTheme = parents.at(i).trimmed(); + + if (!visited.contains(parentTheme)) // guard against recursion + entries = findIconHelper(parentTheme, iconName, visited); + + if (!entries.isEmpty()) // success + break; + } + } + +/********************************************************************* +Author: Kaitlin Rupert +Date: Aug 12, 2010 +Description: Make it so that the QIcon loader honors /usr/share/pixmaps + directory. This is a valid directory per the Freedesktop.org + icon theme specification. +Bug: https://bugreports.qt.nokia.com/browse/QTBUG-12874 + *********************************************************************/ +#if defined(Q_OS_LINUX) || defined(Q_OS_FREEBSD) + /* Freedesktop standard says to look in /usr/share/pixmaps last */ + if (entries.isEmpty()) + { + const QString pixmaps(QLatin1String("/usr/share/pixmaps")); + + QDir currentDir(pixmaps); + QIconDirInfo dirInfo(pixmaps); + if (currentDir.exists(iconName + pngext)) + { + PixmapEntry *iconEntry = new PixmapEntry; + iconEntry->dir = dirInfo; + iconEntry->filename = currentDir.filePath(iconName + pngext); + // Notice we ensure that pixmap entries always come before + // scalable to preserve search order afterwards + entries.prepend(iconEntry); + } + else if (m_supportsSvg && currentDir.exists(iconName + svgext)) + { + ScalableEntry *iconEntry = new ScalableEntry; + iconEntry->dir = dirInfo; + iconEntry->filename = currentDir.filePath(iconName + svgext); + entries.append(iconEntry); + } + else if (currentDir.exists(iconName + xpmext)) + { + PixmapEntry *iconEntry = new PixmapEntry; + iconEntry->dir = dirInfo; + iconEntry->filename = currentDir.filePath(iconName + xpmext); + // Notice we ensure that pixmap entries always come before + // scalable to preserve search order afterwards + entries.append(iconEntry); + } + } +#endif + + if (entries.isEmpty()) + { + // Search for unthemed icons in main dir of search paths + QStringList themeSearchPaths = QIcon::themeSearchPaths(); + foreach (QString contentDir, themeSearchPaths) + { + QDir currentDir(contentDir); + + if (currentDir.exists(iconName + pngext)) + { + PixmapEntry *iconEntry = new PixmapEntry; + iconEntry->filename = currentDir.filePath(iconName + pngext); + // Notice we ensure that pixmap entries always come before + // scalable to preserve search order afterwards + entries.prepend(iconEntry); + } + else if (m_supportsSvg && currentDir.exists(iconName + svgext)) + { + ScalableEntry *iconEntry = new ScalableEntry; + iconEntry->filename = currentDir.filePath(iconName + svgext); + entries.append(iconEntry); + break; + } + else if (currentDir.exists(iconName + xpmext)) + { + PixmapEntry *iconEntry = new PixmapEntry; + iconEntry->filename = currentDir.filePath(iconName + xpmext); + // Notice we ensure that pixmap entries always come before + // scalable to preserve search order afterwards + entries.append(iconEntry); + break; + } + } + } + return entries; +} + +QThemeIconEntries QIconLoader::loadIcon(const QString &name) const +{ + if (!themeName().isEmpty()) + { + QStringList visited; + return findIconHelper(themeName(), name, visited); + } + + return QThemeIconEntries(); +} + +// -------- Icon Loader Engine -------- // + +QIconLoaderEngineFixed::QIconLoaderEngineFixed(const QString &iconName) + : m_iconName(iconName), m_key(0) +{ +} + +QIconLoaderEngineFixed::~QIconLoaderEngineFixed() +{ + qDeleteAll(m_entries); +} + +QIconLoaderEngineFixed::QIconLoaderEngineFixed(const QIconLoaderEngineFixed &other) + : QIconEngine(other), m_iconName(other.m_iconName), m_key(0) +{ +} + +QIconEngine *QIconLoaderEngineFixed::clone() const +{ + return new QIconLoaderEngineFixed(*this); +} + +bool QIconLoaderEngineFixed::read(QDataStream &in) +{ + in >> m_iconName; + return true; +} + +bool QIconLoaderEngineFixed::write(QDataStream &out) const +{ + out << m_iconName; + return true; +} + +bool QIconLoaderEngineFixed::hasIcon() const +{ + return !(m_entries.isEmpty()); +} + +// Lazily load the icon +void QIconLoaderEngineFixed::ensureLoaded() +{ + if (!(QIconLoader::instance()->themeKey() == m_key)) + { + + qDeleteAll(m_entries); + + m_entries = QIconLoader::instance()->loadIcon(m_iconName); + m_key = QIconLoader::instance()->themeKey(); + } +} + +void QIconLoaderEngineFixed::paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, + QIcon::State state) +{ + QSize pixmapSize = rect.size(); +#if defined(Q_WS_MAC) + pixmapSize *= qt_mac_get_scalefactor(); +#endif + painter->drawPixmap(rect, pixmap(pixmapSize, mode, state)); +} + +/* + * This algorithm is defined by the freedesktop spec: + * http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html + */ +static bool directoryMatchesSize(const QIconDirInfo &dir, int iconsize) +{ + if (dir.type == QIconDirInfo::Fixed) + { + return dir.size == iconsize; + } + else if (dir.type == QIconDirInfo::Scalable) + { + return dir.size <= dir.maxSize && iconsize >= dir.minSize; + } + else if (dir.type == QIconDirInfo::Threshold) + { + return iconsize >= dir.size - dir.threshold && iconsize <= dir.size + dir.threshold; + } + + Q_ASSERT(1); // Not a valid value + return false; +} + +/* + * This algorithm is defined by the freedesktop spec: + * http://standards.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html + */ +static int directorySizeDistance(const QIconDirInfo &dir, int iconsize) +{ + if (dir.type == QIconDirInfo::Fixed) + { + return qAbs(dir.size - iconsize); + } + else if (dir.type == QIconDirInfo::Scalable) + { + if (iconsize < dir.minSize) + return dir.minSize - iconsize; + else if (iconsize > dir.maxSize) + return iconsize - dir.maxSize; + else + return 0; + } + else if (dir.type == QIconDirInfo::Threshold) + { + if (iconsize < dir.size - dir.threshold) + return dir.minSize - iconsize; + else if (iconsize > dir.size + dir.threshold) + return iconsize - dir.maxSize; + else + return 0; + } + + Q_ASSERT(1); // Not a valid value + return INT_MAX; +} + +QIconLoaderEngineEntry *QIconLoaderEngineFixed::entryForSize(const QSize &size) +{ + int iconsize = qMin(size.width(), size.height()); + + // Note that m_entries are sorted so that png-files + // come first + + const int numEntries = m_entries.size(); + + // Search for exact matches first + for (int i = 0; i < numEntries; ++i) + { + QIconLoaderEngineEntry *entry = m_entries.at(i); + if (directoryMatchesSize(entry->dir, iconsize)) + { + return entry; + } + } + + // Find the minimum distance icon + int minimalSize = INT_MAX; + QIconLoaderEngineEntry *closestMatch = 0; + for (int i = 0; i < numEntries; ++i) + { + QIconLoaderEngineEntry *entry = m_entries.at(i); + int distance = directorySizeDistance(entry->dir, iconsize); + if (distance < minimalSize) + { + minimalSize = distance; + closestMatch = entry; + } + } + return closestMatch; +} + +/* + * Returns the actual icon size. For scalable svg's this is equivalent + * to the requested size. Otherwise the closest match is returned but + * we can never return a bigger size than the requested size. + * + */ +QSize QIconLoaderEngineFixed::actualSize(const QSize &size, QIcon::Mode mode, + QIcon::State state) +{ + ensureLoaded(); + + QIconLoaderEngineEntry *entry = entryForSize(size); + if (entry) + { + const QIconDirInfo &dir = entry->dir; + if (dir.type == QIconDirInfo::Scalable) + return size; + else + { + int result = qMin(dir.size, qMin(size.width(), size.height())); + return QSize(result, result); + } + } + return QIconEngine::actualSize(size, mode, state); +} + +QPixmap PixmapEntry::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) +{ + Q_UNUSED(state); + + // Ensure that basePixmap is lazily initialized before generating the + // key, otherwise the cache key is not unique + if (basePixmap.isNull()) + basePixmap.load(filename); + + QSize actualSize = basePixmap.size(); + if (!actualSize.isNull() && + (actualSize.width() > size.width() || actualSize.height() > size.height())) + actualSize.scale(size, Qt::KeepAspectRatio); + + QString key = QLatin1String("$qt_theme_") % HexString(basePixmap.cacheKey()) % + HexString(mode) % + HexString(QGuiApplication::palette().cacheKey()) % + HexString(actualSize.width()) % HexString(actualSize.height()); + + QPixmap cachedPixmap; + if (QPixmapCache::find(key, &cachedPixmap)) + { + return cachedPixmap; + } + else + { + if (basePixmap.size() != actualSize) + { + cachedPixmap = basePixmap.scaled(actualSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + } + else + { + cachedPixmap = basePixmap; + } + QPixmapCache::insert(key, cachedPixmap); + } + return cachedPixmap; +} + +QPixmap ScalableEntry::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) +{ + if (svgIcon.isNull()) + { + svgIcon = QIcon(filename); + } + + // Simply reuse svg icon engine + return svgIcon.pixmap(size, mode, state); +} + +QPixmap QIconLoaderEngineFixed::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) +{ + ensureLoaded(); + + QIconLoaderEngineEntry *entry = entryForSize(size); + if (entry) + { + return entry->pixmap(size, mode, state); + } + + return QPixmap(); +} + +QString QIconLoaderEngineFixed::key() const +{ + return QLatin1String("QIconLoaderEngineFixed"); +} + +void QIconLoaderEngineFixed::virtual_hook(int id, void *data) +{ + ensureLoaded(); + + switch (id) + { + case QIconEngine::AvailableSizesHook: + { + QIconEngine::AvailableSizesArgument &arg = + *reinterpret_cast(data); + const int N = m_entries.size(); + QList sizes; + sizes.reserve(N); + + // Gets all sizes from the DirectoryInfo entries + for (int i = 0; i < N; ++i) + { + int size = m_entries.at(i)->dir.size; + sizes.append(QSize(size, size)); + } + arg.sizes.swap(sizes); // commit + } + break; + case QIconEngine::IconNameHook: + { + QString &name = *reinterpret_cast(data); + name = m_iconName; + } + break; + default: + QIconEngine::virtual_hook(id, data); + } +} + +} // QtXdg diff --git a/ultimmc/libraries/iconfix/internal/qiconloader_p.h b/ultimmc/libraries/iconfix/internal/qiconloader_p.h new file mode 100644 index 0000000..e45a08d --- /dev/null +++ b/ultimmc/libraries/iconfix/internal/qiconloader_p.h @@ -0,0 +1,219 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL21$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 or version 3 as published by the Free +** Software Foundation and appearing in the file LICENSE.LGPLv21 and +** LICENSE.LGPLv3 included in the packaging of this file. Please review the +** following information to ensure the GNU Lesser General Public License +** requirements will be met: https://www.gnu.org/licenses/lgpl.html and +** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#pragma once + +#include + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include +#include +#include +#include + + +namespace QtXdg +{ + +class QIconLoader; + +struct QIconDirInfo +{ + enum Type + { + Fixed, + Scalable, + Threshold + }; + QIconDirInfo(const QString &_path = QString()) + : path(_path), size(0), maxSize(0), minSize(0), threshold(0), type(Threshold) + { + } + QString path; + short size; + short maxSize; + short minSize; + short threshold; + Type type : 4; +}; + +class QIconLoaderEngineEntry +{ +public: + virtual ~QIconLoaderEngineEntry() + { + } + virtual QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) = 0; + QString filename; + QIconDirInfo dir; + static int count; +}; + +struct ScalableEntry : public QIconLoaderEngineEntry +{ + QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) Q_DECL_OVERRIDE; + QIcon svgIcon; +}; + +struct PixmapEntry : public QIconLoaderEngineEntry +{ + QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) Q_DECL_OVERRIDE; + QPixmap basePixmap; +}; + +typedef QList QThemeIconEntries; + +// class QIconLoaderEngine : public QIconEngine +class QIconLoaderEngineFixed : public QIconEngine +{ +public: + QIconLoaderEngineFixed(const QString &iconName = QString()); + ~QIconLoaderEngineFixed(); + + void paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state); + QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state); + QSize actualSize(const QSize &size, QIcon::Mode mode, QIcon::State state); + QIconEngine *clone() const; + bool read(QDataStream &in); + bool write(QDataStream &out) const; + +private: + QString key() const; + bool hasIcon() const; + void ensureLoaded(); + void virtual_hook(int id, void *data); + QIconLoaderEngineEntry *entryForSize(const QSize &size); + QIconLoaderEngineFixed(const QIconLoaderEngineFixed &other); + QThemeIconEntries m_entries; + QString m_iconName; + uint m_key; + + friend class QIconLoader; +}; + +class QIconTheme +{ +public: + QIconTheme(const QString &name); + QIconTheme() : m_valid(false) + { + } + QStringList parents() + { + return m_parents; + } + QVector keyList() + { + return m_keyList; + } + QString contentDir() + { + return m_contentDir; + } + QStringList contentDirs() + { + return m_contentDirs; + } + bool isValid() + { + return m_valid; + } + +private: + QString m_contentDir; + QStringList m_contentDirs; + QVector m_keyList; + QStringList m_parents; + bool m_valid; +}; + +class QIconLoader +{ +public: + QIconLoader(); + QThemeIconEntries loadIcon(const QString &iconName) const; + uint themeKey() const + { + return m_themeKey; + } + + QString themeName() const + { + return m_userTheme.isEmpty() ? m_systemTheme : m_userTheme; + } + void setThemeName(const QString &themeName); + QIconTheme theme() + { + return themeList.value(themeName()); + } + void setThemeSearchPath(const QStringList &searchPaths); + QStringList themeSearchPaths() const; + QIconDirInfo dirInfo(int dirindex); + static QIconLoader *instance(); + void updateSystemTheme(); + void invalidateKey() + { + m_themeKey++; + } + void ensureInitialized(); + +private: + QThemeIconEntries findIconHelper(const QString &themeName, const QString &iconName, + QStringList &visited) const; + uint m_themeKey; + bool m_supportsSvg; + bool m_initialized; + + mutable QString m_userTheme; + mutable QString m_systemTheme; + mutable QStringList m_iconDirs; + mutable QHash themeList; +}; + +} // QtXdg + +// Note: class template specialization of 'QTypeInfo' must occur at +// global scope +Q_DECLARE_TYPEINFO(QtXdg::QIconDirInfo, Q_MOVABLE_TYPE); diff --git a/ultimmc/libraries/iconfix/xdgicon.cpp b/ultimmc/libraries/iconfix/xdgicon.cpp new file mode 100644 index 0000000..36fb7d4 --- /dev/null +++ b/ultimmc/libraries/iconfix/xdgicon.cpp @@ -0,0 +1,152 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * Razor - a lightweight, Qt based, desktop toolset + * http://razor-qt.org + * + * Copyright: 2010-2011 Razor team + * Authors: + * Alexander Sokoloff + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#include "xdgicon.h" + +#include +#include +#include +#include +#include +#include +#include "internal/qiconloader_p.h" +#include + +/************************************************ + + ************************************************/ +static void qt_cleanup_icon_cache(); +typedef QCache IconCache; + +namespace +{ +struct QtIconCache : public IconCache +{ + QtIconCache() + { + qAddPostRoutine(qt_cleanup_icon_cache); + } +}; +} +Q_GLOBAL_STATIC(IconCache, qtIconCache) + +static void qt_cleanup_icon_cache() +{ + qtIconCache()->clear(); +} + +/************************************************ + + ************************************************/ +XdgIcon::XdgIcon() +{ +} + +/************************************************ + + ************************************************/ +XdgIcon::~XdgIcon() +{ +} + +/************************************************ + Returns the name of the current icon theme. + ************************************************/ +QString XdgIcon::themeName() +{ + return QIcon::themeName(); +} + +/************************************************ + Sets the current icon theme to name. + ************************************************/ +void XdgIcon::setThemeName(const QString &themeName) +{ + QIcon::setThemeName(themeName); + QtXdg::QIconLoader::instance()->updateSystemTheme(); +} + +/************************************************ + Returns the QIcon corresponding to name in the current icon theme. If no such icon + is found in the current theme fallback is return instead. + ************************************************/ +QIcon XdgIcon::fromTheme(const QString &iconName, const QIcon &fallback) +{ + if (iconName.isEmpty()) + return fallback; + + bool isAbsolute = (iconName[0] == '/'); + + QString name = QFileInfo(iconName).fileName(); + if (name.endsWith(".png", Qt::CaseInsensitive) || + name.endsWith(".svg", Qt::CaseInsensitive) || + name.endsWith(".xpm", Qt::CaseInsensitive)) + { + name.truncate(name.length() - 4); + } + + QIcon icon; + + if (qtIconCache()->contains(name)) + { + icon = *qtIconCache()->object(name); + } + else + { + QIcon *cachedIcon; + if (!isAbsolute) + cachedIcon = new QIcon(new QtXdg::QIconLoaderEngineFixed(name)); + else + cachedIcon = new QIcon(iconName); + qtIconCache()->insert(name, cachedIcon); + icon = *cachedIcon; + } + + // Note the qapp check is to allow lazy loading of static icons + // Supporting fallbacks will not work for this case. + if (qApp && !isAbsolute && icon.availableSizes().isEmpty()) + { + return fallback; + } + return icon; +} + +/************************************************ + Returns the QIcon corresponding to names in the current icon theme. If no such icon + is found in the current theme fallback is return instead. + ************************************************/ +QIcon XdgIcon::fromTheme(const QStringList &iconNames, const QIcon &fallback) +{ + foreach (QString iconName, iconNames) + { + QIcon icon = fromTheme(iconName); + if (!icon.isNull()) + return icon; + } + + return fallback; +} diff --git a/ultimmc/libraries/iconfix/xdgicon.h b/ultimmc/libraries/iconfix/xdgicon.h new file mode 100644 index 0000000..d37eb71 --- /dev/null +++ b/ultimmc/libraries/iconfix/xdgicon.h @@ -0,0 +1,48 @@ +/* BEGIN_COMMON_COPYRIGHT_HEADER + * (c)LGPL2+ + * + * Razor - a lightweight, Qt based, desktop toolset + * http://razor-qt.org + * + * Copyright: 2010-2011 Razor team + * Authors: + * Alexander Sokoloff + * + * This program or library is free software; you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA + * + * END_COMMON_COPYRIGHT_HEADER */ + +#pragma once + +#include +#include +#include + +#include "launcher_iconfix_export.h" + +class LAUNCHER_ICONFIX_EXPORT XdgIcon +{ +public: + static QIcon fromTheme(const QString &iconName, const QIcon &fallback = QIcon()); + static QIcon fromTheme(const QStringList &iconNames, const QIcon &fallback = QIcon()); + + static QString themeName(); + static void setThemeName(const QString &themeName); + +protected: + explicit XdgIcon(); + virtual ~XdgIcon(); +}; diff --git a/ultimmc/libraries/javacheck/.gitignore b/ultimmc/libraries/javacheck/.gitignore new file mode 100644 index 0000000..cc1c52b --- /dev/null +++ b/ultimmc/libraries/javacheck/.gitignore @@ -0,0 +1,6 @@ +.idea +*.iml +out +.classpath +.idea +.project diff --git a/ultimmc/libraries/javacheck/CMakeLists.txt b/ultimmc/libraries/javacheck/CMakeLists.txt new file mode 100644 index 0000000..d0bea2a --- /dev/null +++ b/ultimmc/libraries/javacheck/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.1) +project(launcher Java) +find_package(Java 1.7 REQUIRED COMPONENTS Development) + +include(UseJava) +set(CMAKE_JAVA_JAR_ENTRY_POINT JavaCheck) +set(CMAKE_JAVA_COMPILE_FLAGS -target 7 -source 7 -Xlint:deprecation -Xlint:unchecked) + +set(SRC + JavaCheck.java +) + +add_jar(JavaCheck ${SRC}) +install_jar(JavaCheck "${JARS_DEST_DIR}") diff --git a/ultimmc/libraries/javacheck/JavaCheck.java b/ultimmc/libraries/javacheck/JavaCheck.java new file mode 100644 index 0000000..560abbc --- /dev/null +++ b/ultimmc/libraries/javacheck/JavaCheck.java @@ -0,0 +1,24 @@ +import java.lang.Integer; + +public class JavaCheck +{ + private static final String[] keys = {"os.arch", "java.version", "java.vendor"}; + public static void main (String [] args) + { + int ret = 0; + for(String key : keys) + { + String property = System.getProperty(key); + if(property != null) + { + System.out.println(key + "=" + property); + } + else + { + ret = 1; + } + } + + System.exit(ret); + } +} diff --git a/ultimmc/libraries/katabasis/.gitignore b/ultimmc/libraries/katabasis/.gitignore new file mode 100644 index 0000000..35e189c --- /dev/null +++ b/ultimmc/libraries/katabasis/.gitignore @@ -0,0 +1,2 @@ +build/ +*.kdev4 diff --git a/ultimmc/libraries/katabasis/CMakeLists.txt b/ultimmc/libraries/katabasis/CMakeLists.txt new file mode 100644 index 0000000..c611588 --- /dev/null +++ b/ultimmc/libraries/katabasis/CMakeLists.txt @@ -0,0 +1,53 @@ +cmake_minimum_required(VERSION 3.6) + +string(COMPARE EQUAL "${CMAKE_SOURCE_DIR}" "${CMAKE_BUILD_DIR}" IS_IN_SOURCE_BUILD) +if(IS_IN_SOURCE_BUILD) + message(FATAL_ERROR "You are building Katabasis in-source. Please separate the build tree from the source tree.") +endif() + +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + if(CMAKE_HOST_SYSTEM_VERSION MATCHES ".*[Mm]icrosoft.*" OR + CMAKE_HOST_SYSTEM_VERSION MATCHES ".*WSL.*" + ) + message(FATAL_ERROR "Building Katabasis is not supported in Linux-on-Windows distributions. Use a real Linux machine, not a fraudulent one.") + endif() +endif() + +project(Katabasis) +enable_testing() + +set(CMAKE_AUTOMOC ON) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_CXX_STANDARD_REQUIRED true) +set(CMAKE_C_STANDARD_REQUIRED true) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_C_STANDARD 11) + +find_package(Qt5 COMPONENTS Core Network REQUIRED) + +set( katabasis_PRIVATE + src/DeviceFlow.cpp + src/JsonResponse.cpp + src/JsonResponse.h + src/PollServer.cpp + src/Reply.cpp +) + +set( katabasis_PUBLIC + include/katabasis/DeviceFlow.h + include/katabasis/Globals.h + include/katabasis/PollServer.h + include/katabasis/Reply.h + include/katabasis/RequestParameter.h +) + +add_library( Katabasis STATIC ${katabasis_PRIVATE} ${katabasis_PUBLIC} ) +target_link_libraries(Katabasis Qt5::Core Qt5::Network) + +# needed for statically linked Katabasis in shared libs on x86_64 +set_target_properties(Katabasis + PROPERTIES POSITION_INDEPENDENT_CODE TRUE +) + +target_include_directories(Katabasis PUBLIC include PRIVATE src include/katabasis) diff --git a/ultimmc/libraries/katabasis/LICENSE b/ultimmc/libraries/katabasis/LICENSE new file mode 100644 index 0000000..9ac8d42 --- /dev/null +++ b/ultimmc/libraries/katabasis/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2012, Akos Polster +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ultimmc/libraries/katabasis/README.md b/ultimmc/libraries/katabasis/README.md new file mode 100644 index 0000000..a4dc099 --- /dev/null +++ b/ultimmc/libraries/katabasis/README.md @@ -0,0 +1,36 @@ +# Katabasis - MS-flavoerd OAuth for Qt, derived from the O2 library + +This library's sole purpose is to make interacting with MSA and various MSA and XBox authenticated services less painful. + +It may be possible to backport some of the changes to O2 in the future, but for the sake of going fast, all compatibility concerns have been ignored. + +[You can find the original library's git repository here.](https://github.com/pipacs/o2) + +Notes to contributors: + + * Please follow the coding style of the existing source, where reasonable + * Code contributions are released under Simplified BSD License, as specified in LICENSE. Do not contribute if this license does not suit your code + * If you are interested in working on this, come to the MultiMC Discord server and talk first + +## Installation + +Clone the Github repository, integrate the it into your CMake build system. + +The library is static only, dynamic linking and system-wide installation are out of scope and undesirable. + +## Usage + +At this stage, don't, unless you want to help with the library itself. + +This is an experimental fork of the O2 library and is undergoing a big design/architecture shift in order to support different features: + +* Multiple accounts +* Multi-stage authentication/authorization schemes +* Tighter control over token chains and their storage +* Talking to complex APIs and individually authorized microservices +* Token lifetime management, 'offline mode' and resilience in face of network failures +* Token and claims/entitlements validation +* Caching of some API results +* XBox magic +* Mojang magic +* Generally, magic that you would spend weeks on researching while getting confused by contradictory/incomplete documentation (if any is available) diff --git a/ultimmc/libraries/katabasis/acknowledgements.md b/ultimmc/libraries/katabasis/acknowledgements.md new file mode 100644 index 0000000..c1c8a3d --- /dev/null +++ b/ultimmc/libraries/katabasis/acknowledgements.md @@ -0,0 +1,110 @@ +# O2 library by Akos Polster and contributors + +[The origin of this fork.](https://github.com/pipacs/o2) + +> Copyright (c) 2012, Akos Polster +> All rights reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions are met: +> +> * Redistributions of source code must retain the above copyright notice, this +> list of conditions and the following disclaimer. +> +> * Redistributions in binary form must reproduce the above copyright notice, +> this list of conditions and the following disclaimer in the documentation +> and/or other materials provided with the distribution. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +> AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +> IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +> DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +> FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +> DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +> SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +> OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +> OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +# SimpleCrypt by Andre Somers + +Cryptographic methods for Qt. + +> Copyright (c) 2011, Andre Somers +> All rights reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions are met: +> +> * Redistributions of source code must retain the above copyright +> notice, this list of conditions and the following disclaimer. +> * Redistributions in binary form must reproduce the above copyright +> notice, this list of conditions and the following disclaimer in the +> documentation and/or other materials provided with the distribution. +> * Neither the name of the Rathenau Instituut, Andre Somers nor the +> names of its contributors may be used to endorse or promote products +> derived from this software without specific prior written permission. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +> ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +> WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +> DISCLAIMED. IN NO EVENT SHALL ANDRE SOMERS BE LIABLE FOR ANY +> DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +> (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +> ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +> (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +> SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# Mandeep Sandhu + +Configurable settings storage, Twitter XAuth specialization, new demos, cleanups. + +> "Hi Akos, +> +> I'm writing this mail to confirm that my contributions to the O2 library, available here https://github.com/pipacs/o2, can be freely distributed according to the project's license (as shown in the LICENSE file). +> +> Regards, +> -mandeep" + +# Sergey Gavrushkin + +FreshBooks specialization + +# Theofilos Intzoglou + +Hubic specialization + +# Dimitar + +SurveyMonkey specialization + +# David Brooks + +CMake related fixes and improvements. + +# Lukas Vogel + +Spotify support + +# Alan Garny + +Windows DLL build support + +# MartinMikita + +Bug fixes + +# Larry Shaffer + +Versioning, shared lib, install target and header support + +# Gilmanov Ildar + +Bug fixes, support for ```qml``` module + +# Fabian Vogt + +Bug fixes, support for building without Qt keywords enabled + diff --git a/ultimmc/libraries/katabasis/include/katabasis/Bits.h b/ultimmc/libraries/katabasis/include/katabasis/Bits.h new file mode 100644 index 0000000..f11f25d --- /dev/null +++ b/ultimmc/libraries/katabasis/include/katabasis/Bits.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include + +namespace Katabasis { +enum class Activity { + Idle, + LoggingIn, + LoggingOut, + Refreshing, + FailedSoft, //!< soft failure. this generally means the user auth details haven't been invalidated + FailedHard, //!< hard failure. auth is invalid + FailedGone, //!< hard failure. auth is invalid, and the account no longer exists + Succeeded +}; + +enum class Validity { + None, + Assumed, + Certain +}; + +struct Token { + QDateTime issueInstant; + QDateTime notAfter; + QString token; + QString refresh_token; + QVariantMap extra; + + Validity validity = Validity::None; + bool persistent = true; +}; + +} diff --git a/ultimmc/libraries/katabasis/include/katabasis/DeviceFlow.h b/ultimmc/libraries/katabasis/include/katabasis/DeviceFlow.h new file mode 100644 index 0000000..b68c92e --- /dev/null +++ b/ultimmc/libraries/katabasis/include/katabasis/DeviceFlow.h @@ -0,0 +1,150 @@ +#pragma once + +#include +#include +#include +#include + +#include "Reply.h" +#include "RequestParameter.h" +#include "Bits.h" + +namespace Katabasis { + +class ReplyServer; +class PollServer; + +/// Simple OAuth2 Device Flow authenticator. +class DeviceFlow: public QObject +{ + Q_OBJECT +public: + Q_ENUMS(GrantFlow) + +public: + + struct Options { + QString userAgent = QStringLiteral("Katabasis/1.0"); + QString responseType = QStringLiteral("code"); + QString scope; + QString clientIdentifier; + QString clientSecret; + QUrl authorizationUrl; + QUrl accessTokenUrl; + }; + +public: + /// Are we authenticated? + bool linked(); + + /// Authentication token. + QString token(); + + /// Provider-specific extra tokens, available after a successful authentication + QVariantMap extraTokens(); + +public: + // TODO: put in `Options` + /// User-defined extra parameters to append to request URL + QVariantMap extraRequestParams(); + void setExtraRequestParams(const QVariantMap &value); + + // TODO: split up the class into multiple, each implementing one OAuth2 flow + /// Grant type (if non-standard) + QString grantType(); + void setGrantType(const QString &value); + +public: + /// Constructor. + /// @param parent Parent object. + explicit DeviceFlow(Options & opts, Token & token, QObject *parent = 0, QNetworkAccessManager *manager = 0); + + /// Get refresh token. + QString refreshToken(); + + /// Get token expiration time + QDateTime expires(); + +public slots: + /// Authenticate. + void login(); + + /// De-authenticate. + void logout(); + + /// Refresh token. + bool refresh(); + + /// Handle situation where reply server has opted to close its connection + void serverHasClosed(bool paramsfound = false); + +signals: + /// Emitted when client needs to open a web browser window, with the given URL. + void openBrowser(const QUrl &url); + + /// Emitted when client can close the browser window. + void closeBrowser(); + + /// Emitted when client needs to show a verification uri and user code + void showVerificationUriAndCode(const QUrl &uri, const QString &code, int expiresIn); + + /// Emitted when the internal state changes + void activityChanged(Activity activity); + +public slots: + /// Handle verification response. + void onVerificationReceived(QMap); + +protected slots: + /// Handle completion of a Device Authorization Request + void onDeviceAuthReplyFinished(); + + /// Handle completion of a refresh request. + void onRefreshFinished(); + + /// Handle failure of a refresh request. + void onRefreshError(QNetworkReply::NetworkError error, QNetworkReply *reply); + +protected: + /// Set refresh token. + void setRefreshToken(const QString &v); + + /// Set token expiration time. + void setExpires(QDateTime v); + + /// Start polling authorization server + void startPollServer(const QVariantMap ¶ms, int expiresIn); + + /// Set authentication token. + void setToken(const QString &v); + + /// Set the linked state + void setLinked(bool v); + + /// Set extra tokens found in OAuth response + void setExtraTokens(QVariantMap extraTokens); + + /// Set local poll server + void setPollServer(PollServer *server); + + PollServer * pollServer() const; + + void updateActivity(Activity activity); + +protected: + Options options_; + + QVariantMap extraReqParams_; + QNetworkAccessManager *manager_ = nullptr; + ReplyList timedReplies_; + QString grantType_; + +protected: + Token &token_; + +private: + PollServer *pollServer_ = nullptr; + Activity activity_ = Activity::Idle; +}; + +} diff --git a/ultimmc/libraries/katabasis/include/katabasis/Globals.h b/ultimmc/libraries/katabasis/include/katabasis/Globals.h new file mode 100644 index 0000000..512745d --- /dev/null +++ b/ultimmc/libraries/katabasis/include/katabasis/Globals.h @@ -0,0 +1,59 @@ +#pragma once + +namespace Katabasis { + +// Common constants +const char ENCRYPTION_KEY[] = "12345678"; +const char MIME_TYPE_XFORM[] = "application/x-www-form-urlencoded"; +const char MIME_TYPE_JSON[] = "application/json"; + +// OAuth 1/1.1 Request Parameters +const char OAUTH_CALLBACK[] = "oauth_callback"; +const char OAUTH_CONSUMER_KEY[] = "oauth_consumer_key"; +const char OAUTH_NONCE[] = "oauth_nonce"; +const char OAUTH_SIGNATURE[] = "oauth_signature"; +const char OAUTH_SIGNATURE_METHOD[] = "oauth_signature_method"; +const char OAUTH_TIMESTAMP[] = "oauth_timestamp"; +const char OAUTH_VERSION[] = "oauth_version"; +// OAuth 1/1.1 Response Parameters +const char OAUTH_TOKEN[] = "oauth_token"; +const char OAUTH_TOKEN_SECRET[] = "oauth_token_secret"; +const char OAUTH_CALLBACK_CONFIRMED[] = "oauth_callback_confirmed"; +const char OAUTH_VERFIER[] = "oauth_verifier"; + +// OAuth 2 Request Parameters +const char OAUTH2_RESPONSE_TYPE[] = "response_type"; +const char OAUTH2_CLIENT_ID[] = "client_id"; +const char OAUTH2_CLIENT_SECRET[] = "client_secret"; +const char OAUTH2_USERNAME[] = "username"; +const char OAUTH2_PASSWORD[] = "password"; +const char OAUTH2_REDIRECT_URI[] = "redirect_uri"; +const char OAUTH2_SCOPE[] = "scope"; +const char OAUTH2_GRANT_TYPE_CODE[] = "code"; +const char OAUTH2_GRANT_TYPE_TOKEN[] = "token"; +const char OAUTH2_GRANT_TYPE_PASSWORD[] = "password"; +const char OAUTH2_GRANT_TYPE_DEVICE[] = "urn:ietf:params:oauth:grant-type:device_code"; +const char OAUTH2_GRANT_TYPE[] = "grant_type"; +const char OAUTH2_API_KEY[] = "api_key"; +const char OAUTH2_STATE[] = "state"; +const char OAUTH2_CODE[] = "code"; + +// OAuth 2 Response Parameters +const char OAUTH2_ACCESS_TOKEN[] = "access_token"; +const char OAUTH2_REFRESH_TOKEN[] = "refresh_token"; +const char OAUTH2_EXPIRES_IN[] = "expires_in"; +const char OAUTH2_DEVICE_CODE[] = "device_code"; +const char OAUTH2_USER_CODE[] = "user_code"; +const char OAUTH2_VERIFICATION_URI[] = "verification_uri"; +const char OAUTH2_VERIFICATION_URL[] = "verification_url"; // Google sign-in +const char OAUTH2_VERIFICATION_URI_COMPLETE[] = "verification_uri_complete"; +const char OAUTH2_INTERVAL[] = "interval"; + +// Parameter values +const char AUTHORIZATION_CODE[] = "authorization_code"; + +// Standard HTTP headers +const char HTTP_HTTP_HEADER[] = "HTTP"; +const char HTTP_AUTHORIZATION_HEADER[] = "Authorization"; + +} diff --git a/ultimmc/libraries/katabasis/include/katabasis/PollServer.h b/ultimmc/libraries/katabasis/include/katabasis/PollServer.h new file mode 100644 index 0000000..7710386 --- /dev/null +++ b/ultimmc/libraries/katabasis/include/katabasis/PollServer.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +class QNetworkAccessManager; + +namespace Katabasis { + +/// Poll an authorization server for token +class PollServer : public QObject +{ + Q_OBJECT + +public: + explicit PollServer(QNetworkAccessManager * manager, const QNetworkRequest &request, const QByteArray & payload, int expiresIn, QObject *parent = 0); + + /// Seconds to wait between polling requests + Q_PROPERTY(int interval READ interval WRITE setInterval) + int interval() const; + void setInterval(int interval); + +signals: + void verificationReceived(QMap); + void serverClosed(bool); // whether it has found parameters + +public slots: + void startPolling(); + +protected slots: + void onPollTimeout(); + void onExpiration(); + void onReplyFinished(); + +protected: + QNetworkAccessManager *manager_; + const QNetworkRequest request_; + const QByteArray payload_; + const int expiresIn_; + QTimer expirationTimer; + QTimer pollTimer; +}; + +} diff --git a/ultimmc/libraries/katabasis/include/katabasis/Reply.h b/ultimmc/libraries/katabasis/include/katabasis/Reply.h new file mode 100644 index 0000000..415cf4e --- /dev/null +++ b/ultimmc/libraries/katabasis/include/katabasis/Reply.h @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Katabasis { + +constexpr int defaultTimeout = 30 * 1000; + +/// A network request/reply pair that can time out. +class Reply: public QTimer { + Q_OBJECT + +public: + Reply(QNetworkReply *reply, int timeOut = defaultTimeout, QObject *parent = 0); + +signals: + void error(QNetworkReply::NetworkError); + +public slots: + /// When time out occurs, the QNetworkReply's error() signal is triggered. + void onTimeOut(); + +public: + QNetworkReply *reply; + bool timedOut = false; +}; + +/// List of O2Replies. +class ReplyList { +public: + ReplyList() { ignoreSslErrors_ = false; } + + /// Destructor. + /// Deletes all O2Reply instances in the list. + virtual ~ReplyList(); + + /// Create a new O2Reply from a QNetworkReply, and add it to this list. + void add(QNetworkReply *reply, int timeOut = defaultTimeout); + + /// Add an O2Reply to the list, while taking ownership of it. + void add(Reply *reply); + + /// Remove item from the list that corresponds to a QNetworkReply. + void remove(QNetworkReply *reply); + + /// Find an O2Reply in the list, corresponding to a QNetworkReply. + /// @return Matching O2Reply or NULL. + Reply *find(QNetworkReply *reply); + + bool ignoreSslErrors(); + void setIgnoreSslErrors(bool ignoreSslErrors); + +protected: + QList replies_; + bool ignoreSslErrors_; +}; + +} diff --git a/ultimmc/libraries/katabasis/include/katabasis/RequestParameter.h b/ultimmc/libraries/katabasis/include/katabasis/RequestParameter.h new file mode 100644 index 0000000..ca36934 --- /dev/null +++ b/ultimmc/libraries/katabasis/include/katabasis/RequestParameter.h @@ -0,0 +1,15 @@ +#pragma once + +namespace Katabasis { + +/// Request parameter (name-value pair) participating in authentication. +struct RequestParameter { + RequestParameter(const QByteArray &n, const QByteArray &v): name(n), value(v) {} + bool operator <(const RequestParameter &other) const { + return (name == other.name)? (value < other.value): (name < other.name); + } + QByteArray name; + QByteArray value; +}; + +} diff --git a/ultimmc/libraries/katabasis/src/DeviceFlow.cpp b/ultimmc/libraries/katabasis/src/DeviceFlow.cpp new file mode 100644 index 0000000..ba1d121 --- /dev/null +++ b/ultimmc/libraries/katabasis/src/DeviceFlow.cpp @@ -0,0 +1,451 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "katabasis/DeviceFlow.h" +#include "katabasis/PollServer.h" +#include "katabasis/Globals.h" + +#include "JsonResponse.h" + +namespace { +// ref: https://tools.ietf.org/html/rfc8628#section-3.2 +// Exception: Google sign-in uses "verification_url" instead of "*_uri" - we'll accept both. +bool hasMandatoryDeviceAuthParams(const QVariantMap& params) +{ + if (!params.contains(Katabasis::OAUTH2_DEVICE_CODE)) + return false; + + if (!params.contains(Katabasis::OAUTH2_USER_CODE)) + return false; + + if (!(params.contains(Katabasis::OAUTH2_VERIFICATION_URI) || params.contains(Katabasis::OAUTH2_VERIFICATION_URL))) + return false; + + if (!params.contains(Katabasis::OAUTH2_EXPIRES_IN)) + return false; + + return true; +} + +QByteArray createQueryParameters(const QList ¶meters) { + QByteArray ret; + bool first = true; + for( auto & h: parameters) { + if (first) { + first = false; + } else { + ret.append("&"); + } + ret.append(QUrl::toPercentEncoding(h.name) + "=" + QUrl::toPercentEncoding(h.value)); + } + return ret; +} +} + +namespace Katabasis { + +DeviceFlow::DeviceFlow(Options & opts, Token & token, QObject *parent, QNetworkAccessManager *manager) : QObject(parent), token_(token) { + manager_ = manager ? manager : new QNetworkAccessManager(this); + qRegisterMetaType("QNetworkReply::NetworkError"); + options_ = opts; +} + +bool DeviceFlow::linked() { + return token_.validity != Validity::None; +} +void DeviceFlow::setLinked(bool v) { + qDebug() << "DeviceFlow::setLinked:" << (v? "true": "false"); + token_.validity = v ? Validity::Certain : Validity::None; +} + +void DeviceFlow::updateActivity(Activity activity) +{ + if(activity_ == activity) { + return; + } + + activity_ = activity; + switch(activity) { + case Katabasis::Activity::Idle: + case Katabasis::Activity::LoggingIn: + case Katabasis::Activity::LoggingOut: + case Katabasis::Activity::Refreshing: + // non-terminal states... + break; + case Katabasis::Activity::FailedSoft: + // terminal state, tokens did not change + break; + case Katabasis::Activity::FailedHard: + case Katabasis::Activity::FailedGone: + // terminal state, tokens are invalid + token_ = Token(); + break; + case Katabasis::Activity::Succeeded: + setLinked(true); + break; + } + emit activityChanged(activity_); +} + +QString DeviceFlow::token() { + return token_.token; +} +void DeviceFlow::setToken(const QString &v) { + token_.token = v; +} + +QVariantMap DeviceFlow::extraTokens() { + return token_.extra; +} + +void DeviceFlow::setExtraTokens(QVariantMap extraTokens) { + token_.extra = extraTokens; +} + +void DeviceFlow::setPollServer(PollServer *server) +{ + if (pollServer_) + pollServer_->deleteLater(); + + pollServer_ = server; +} + +PollServer *DeviceFlow::pollServer() const +{ + return pollServer_; +} + +QVariantMap DeviceFlow::extraRequestParams() +{ + return extraReqParams_; +} + +void DeviceFlow::setExtraRequestParams(const QVariantMap &value) +{ + extraReqParams_ = value; +} + +QString DeviceFlow::grantType() +{ + if (!grantType_.isEmpty()) + return grantType_; + + return OAUTH2_GRANT_TYPE_DEVICE; +} + +void DeviceFlow::setGrantType(const QString &value) +{ + grantType_ = value; +} + +// First get the URL and token to display to the user +void DeviceFlow::login() { + qDebug() << "DeviceFlow::link"; + + updateActivity(Activity::LoggingIn); + setLinked(false); + setToken(""); + setExtraTokens(QVariantMap()); + setRefreshToken(QString()); + setExpires(QDateTime()); + + QList parameters; + parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8())); + parameters.append(RequestParameter(OAUTH2_SCOPE, options_.scope.toUtf8())); + QByteArray payload = createQueryParameters(parameters); + + QUrl url(options_.authorizationUrl); + QNetworkRequest deviceRequest(url); + deviceRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + QNetworkReply *tokenReply = manager_->post(deviceRequest, payload); + + connect(tokenReply, &QNetworkReply::finished, this, &DeviceFlow::onDeviceAuthReplyFinished, Qt::QueuedConnection); +} + +// Then, once we get them, present them to the user +void DeviceFlow::onDeviceAuthReplyFinished() +{ + qDebug() << "DeviceFlow::onDeviceAuthReplyFinished"; + QNetworkReply *tokenReply = qobject_cast(sender()); + if (!tokenReply) + { + qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: reply is null"; + return; + } + if (tokenReply->error() == QNetworkReply::NoError) { + QByteArray replyData = tokenReply->readAll(); + + // Dump replyData + // SENSITIVE DATA in RelWithDebInfo or Debug builds + //qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: replyData\n"; + //qDebug() << QString( replyData ); + + QVariantMap params = parseJsonResponse(replyData); + + // Dump tokens + qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: Tokens returned:\n"; + foreach (QString key, params.keys()) { + // SENSITIVE DATA in RelWithDebInfo or Debug builds, so it is truncated first + qDebug() << key << ": "<< params.value( key ).toString(); + } + + // Check for mandatory parameters + if (hasMandatoryDeviceAuthParams(params)) { + qDebug() << "DeviceFlow::onDeviceAuthReplyFinished: Device auth request response"; + + const QString userCode = params.take(OAUTH2_USER_CODE).toString(); + QUrl uri = params.take(OAUTH2_VERIFICATION_URI).toUrl(); + if (uri.isEmpty()) + uri = params.take(OAUTH2_VERIFICATION_URL).toUrl(); + + if (params.contains(OAUTH2_VERIFICATION_URI_COMPLETE)) + emit openBrowser(params.take(OAUTH2_VERIFICATION_URI_COMPLETE).toUrl()); + + bool ok = false; + int expiresIn = params[OAUTH2_EXPIRES_IN].toInt(&ok); + if (!ok) { + qWarning() << "DeviceFlow::startPollServer: No expired_in parameter"; + updateActivity(Activity::FailedHard); + return; + } + + emit showVerificationUriAndCode(uri, userCode, expiresIn); + + startPollServer(params, expiresIn); + } else { + qWarning() << "DeviceFlow::onDeviceAuthReplyFinished: Mandatory parameters missing from response"; + updateActivity(Activity::FailedHard); + } + } + tokenReply->deleteLater(); +} + +// Spin up polling for the user completing the login flow out of band +void DeviceFlow::startPollServer(const QVariantMap ¶ms, int expiresIn) +{ + qDebug() << "DeviceFlow::startPollServer: device_ and user_code expires in" << expiresIn << "seconds"; + + QUrl url(options_.accessTokenUrl); + QNetworkRequest authRequest(url); + authRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); + + const QString deviceCode = params[OAUTH2_DEVICE_CODE].toString(); + const QString grantType = grantType_.isEmpty() ? OAUTH2_GRANT_TYPE_DEVICE : grantType_; + + QList parameters; + parameters.append(RequestParameter(OAUTH2_CLIENT_ID, options_.clientIdentifier.toUtf8())); + if ( !options_.clientSecret.isEmpty() ) { + parameters.append(RequestParameter(OAUTH2_CLIENT_SECRET, options_.clientSecret.toUtf8())); + } + parameters.append(RequestParameter(OAUTH2_CODE, deviceCode.toUtf8())); + parameters.append(RequestParameter(OAUTH2_GRANT_TYPE, grantType.toUtf8())); + QByteArray payload = createQueryParameters(parameters); + + PollServer * pollServer = new PollServer(manager_, authRequest, payload, expiresIn, this); + if (params.contains(OAUTH2_INTERVAL)) { + bool ok = false; + int interval = params[OAUTH2_INTERVAL].toInt(&ok); + if (ok) { + pollServer->setInterval(interval); + } + } + connect(pollServer, &PollServer::verificationReceived, this, &DeviceFlow::onVerificationReceived); + connect(pollServer, &PollServer::serverClosed, this, &DeviceFlow::serverHasClosed); + setPollServer(pollServer); + pollServer->startPolling(); +} + +// Once the user completes the flow, update the internal state and report it to observers +void DeviceFlow::onVerificationReceived(const QMap response) { + qDebug() << "DeviceFlow::onVerificationReceived: Emitting closeBrowser()"; + emit closeBrowser(); + + if (response.contains("error")) { + qWarning() << "DeviceFlow::onVerificationReceived: Verification failed:" << response; + updateActivity(Activity::FailedHard); + return; + } + + // Check for mandatory tokens + if (response.contains(OAUTH2_ACCESS_TOKEN)) { + qDebug() << "DeviceFlow::onVerificationReceived: Access token returned for implicit or device flow"; + setToken(response.value(OAUTH2_ACCESS_TOKEN)); + if (response.contains(OAUTH2_EXPIRES_IN)) { + bool ok = false; + int expiresIn = response.value(OAUTH2_EXPIRES_IN).toInt(&ok); + if (ok) { + qDebug() << "DeviceFlow::onVerificationReceived: Token expires in" << expiresIn << "seconds"; + setExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn)); + } + } + if (response.contains(OAUTH2_REFRESH_TOKEN)) { + setRefreshToken(response.value(OAUTH2_REFRESH_TOKEN)); + } + updateActivity(Activity::Succeeded); + } else { + qWarning() << "DeviceFlow::onVerificationReceived: Access token missing from response for implicit or device flow"; + updateActivity(Activity::FailedHard); + } +} + +// Or if the flow fails or the polling times out, update the internal state with error and report it to observers +void DeviceFlow::serverHasClosed(bool paramsfound) +{ + if ( !paramsfound ) { + // server has probably timed out after receiving first response + updateActivity(Activity::FailedHard); + } + // poll server is not re-used for later auth requests + setPollServer(NULL); +} + +void DeviceFlow::logout() { + qDebug() << "DeviceFlow::unlink"; + updateActivity(Activity::LoggingOut); + // FIXME: implement logout flows... if they exist + token_ = Token(); + updateActivity(Activity::FailedHard); +} + +QDateTime DeviceFlow::expires() { + return token_.notAfter; +} +void DeviceFlow::setExpires(QDateTime v) { + token_.notAfter = v; +} + +QString DeviceFlow::refreshToken() { + return token_.refresh_token; +} + +void DeviceFlow::setRefreshToken(const QString &v) { +#ifndef NDEBUG + qDebug() << "DeviceFlow::setRefreshToken" << v << "..."; +#endif + token_.refresh_token = v; +} + +namespace { +QByteArray buildRequestBody(const QMap ¶meters) { + QByteArray body; + bool first = true; + foreach (QString key, parameters.keys()) { + if (first) { + first = false; + } else { + body.append("&"); + } + QString value = parameters.value(key); + body.append(QUrl::toPercentEncoding(key) + QString("=").toUtf8() + QUrl::toPercentEncoding(value)); + } + return body; +} +} + +bool DeviceFlow::refresh() { + qDebug() << "DeviceFlow::refresh: Token: ..." << refreshToken().right(7); + + updateActivity(Activity::Refreshing); + + if (refreshToken().isEmpty()) { + qWarning() << "DeviceFlow::refresh: No refresh token"; + onRefreshError(QNetworkReply::AuthenticationRequiredError, nullptr); + return false; + } + if (options_.accessTokenUrl.isEmpty()) { + qWarning() << "DeviceFlow::refresh: Refresh token URL not set"; + onRefreshError(QNetworkReply::AuthenticationRequiredError, nullptr); + return false; + } + + QNetworkRequest refreshRequest(options_.accessTokenUrl); + refreshRequest.setHeader(QNetworkRequest::ContentTypeHeader, MIME_TYPE_XFORM); + QMap parameters; + parameters.insert(OAUTH2_CLIENT_ID, options_.clientIdentifier); + if ( !options_.clientSecret.isEmpty() ) { + parameters.insert(OAUTH2_CLIENT_SECRET, options_.clientSecret); + } + parameters.insert(OAUTH2_REFRESH_TOKEN, refreshToken()); + parameters.insert(OAUTH2_GRANT_TYPE, OAUTH2_REFRESH_TOKEN); + + QByteArray data = buildRequestBody(parameters); + QNetworkReply *refreshReply = manager_->post(refreshRequest, data); + timedReplies_.add(refreshReply); + connect(refreshReply, &QNetworkReply::finished, this, &DeviceFlow::onRefreshFinished, Qt::QueuedConnection); + return true; +} + +void DeviceFlow::onRefreshFinished() { + QNetworkReply *refreshReply = qobject_cast(sender()); + + auto networkError = refreshReply->error(); + if (networkError == QNetworkReply::NoError) { + QByteArray reply = refreshReply->readAll(); + QVariantMap tokens = parseJsonResponse(reply); + setToken(tokens.value(OAUTH2_ACCESS_TOKEN).toString()); + setExpires(QDateTime::currentDateTimeUtc().addSecs(tokens.value(OAUTH2_EXPIRES_IN).toInt())); + QString refreshToken = tokens.value(OAUTH2_REFRESH_TOKEN).toString(); + if(!refreshToken.isEmpty()) { + setRefreshToken(refreshToken); + } + else { + qDebug() << "No new refresh token. Keep the old one."; + } + timedReplies_.remove(refreshReply); + refreshReply->deleteLater(); + updateActivity(Activity::Succeeded); + qDebug() << "New token expires in" << expires() << "seconds"; + } else { + // FIXME: differentiate the error more here + onRefreshError(networkError, refreshReply); + } +} + +void DeviceFlow::onRefreshError(QNetworkReply::NetworkError error, QNetworkReply *refreshReply) { + QString errorString = "No Reply"; + if(refreshReply) { + timedReplies_.remove(refreshReply); + errorString = refreshReply->errorString(); + } + + switch (error) + { + // used for invalid credentials and similar errors. Fall through. + case QNetworkReply::AuthenticationRequiredError: + case QNetworkReply::ContentAccessDenied: + case QNetworkReply::ContentOperationNotPermittedError: + case QNetworkReply::ProtocolInvalidOperationError: + updateActivity(Activity::FailedHard); + break; + case QNetworkReply::ContentGoneError: { + updateActivity(Activity::FailedGone); + break; + } + case QNetworkReply::TimeoutError: + case QNetworkReply::OperationCanceledError: + case QNetworkReply::SslHandshakeFailedError: + default: + updateActivity(Activity::FailedSoft); + return; + } + if(refreshReply) { + refreshReply->deleteLater(); + } + qDebug() << "DeviceFlow::onRefreshFinished: Error" << (int)error << " - " << errorString; +} + +} diff --git a/ultimmc/libraries/katabasis/src/JsonResponse.cpp b/ultimmc/libraries/katabasis/src/JsonResponse.cpp new file mode 100644 index 0000000..63384d1 --- /dev/null +++ b/ultimmc/libraries/katabasis/src/JsonResponse.cpp @@ -0,0 +1,26 @@ +#include "JsonResponse.h" + +#include +#include +#include +#include + +namespace Katabasis { + +QVariantMap parseJsonResponse(const QByteArray &data) { + QJsonParseError err; + QJsonDocument doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError) { + qWarning() << "parseTokenResponse: Failed to parse token response due to err:" << err.errorString(); + return QVariantMap(); + } + + if (!doc.isObject()) { + qWarning() << "parseTokenResponse: Token response is not an object"; + return QVariantMap(); + } + + return doc.object().toVariantMap(); +} + +} diff --git a/ultimmc/libraries/katabasis/src/JsonResponse.h b/ultimmc/libraries/katabasis/src/JsonResponse.h new file mode 100644 index 0000000..e7fe7e3 --- /dev/null +++ b/ultimmc/libraries/katabasis/src/JsonResponse.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +class QByteArray; + +namespace Katabasis { + + /// Parse JSON data into a QVariantMap +QVariantMap parseJsonResponse(const QByteArray &data); + +} diff --git a/ultimmc/libraries/katabasis/src/PollServer.cpp b/ultimmc/libraries/katabasis/src/PollServer.cpp new file mode 100644 index 0000000..1083c59 --- /dev/null +++ b/ultimmc/libraries/katabasis/src/PollServer.cpp @@ -0,0 +1,123 @@ +#include +#include + +#include "katabasis/PollServer.h" +#include "JsonResponse.h" + +namespace { +QMap toVerificationParams(const QVariantMap &map) +{ + QMap params; + for (QVariantMap::const_iterator i = map.constBegin(); + i != map.constEnd(); ++i) + { + params[i.key()] = i.value().toString(); + } + return params; +} +} + +namespace Katabasis { + +PollServer::PollServer(QNetworkAccessManager *manager, const QNetworkRequest &request, const QByteArray &payload, int expiresIn, QObject *parent) + : QObject(parent) + , manager_(manager) + , request_(request) + , payload_(payload) + , expiresIn_(expiresIn) +{ + expirationTimer.setTimerType(Qt::VeryCoarseTimer); + expirationTimer.setInterval(expiresIn * 1000); + expirationTimer.setSingleShot(true); + connect(&expirationTimer, SIGNAL(timeout()), this, SLOT(onExpiration())); + expirationTimer.start(); + + pollTimer.setTimerType(Qt::VeryCoarseTimer); + pollTimer.setInterval(5 * 1000); + pollTimer.setSingleShot(true); + connect(&pollTimer, SIGNAL(timeout()), this, SLOT(onPollTimeout())); +} + +int PollServer::interval() const +{ + return pollTimer.interval() / 1000; +} + +void PollServer::setInterval(int interval) +{ + pollTimer.setInterval(interval * 1000); +} + +void PollServer::startPolling() +{ + if (expirationTimer.isActive()) { + pollTimer.start(); + } +} + +void PollServer::onPollTimeout() +{ + qDebug() << "PollServer::onPollTimeout: retrying"; + QNetworkReply * reply = manager_->post(request_, payload_); + connect(reply, SIGNAL(finished()), this, SLOT(onReplyFinished())); +} + +void PollServer::onExpiration() +{ + pollTimer.stop(); + emit serverClosed(false); +} + +void PollServer::onReplyFinished() +{ + QNetworkReply *reply = qobject_cast(sender()); + + if (!reply) { + qDebug() << "PollServer::onReplyFinished: reply is null"; + return; + } + + QByteArray replyData = reply->readAll(); + QMap params = toVerificationParams(parseJsonResponse(replyData)); + + // Dump replyData + // SENSITIVE DATA in RelWithDebInfo or Debug builds + // qDebug() << "PollServer::onReplyFinished: replyData\n"; + // qDebug() << QString( replyData ); + + if (reply->error() == QNetworkReply::TimeoutError) { + // rfc8628#section-3.2 + // "On encountering a connection timeout, clients MUST unilaterally + // reduce their polling frequency before retrying. The use of an + // exponential backoff algorithm to achieve this, such as doubling the + // polling interval on each such connection timeout, is RECOMMENDED." + setInterval(interval() * 2); + pollTimer.start(); + } + else { + QString error = params.value("error"); + if (error == "slow_down") { + // rfc8628#section-3.2 + // "A variant of 'authorization_pending', the authorization request is + // still pending and polling should continue, but the interval MUST + // be increased by 5 seconds for this and all subsequent requests." + setInterval(interval() + 5); + pollTimer.start(); + } + else if (error == "authorization_pending") { + // keep trying - rfc8628#section-3.2 + // "The authorization request is still pending as the end user hasn't + // yet completed the user-interaction steps (Section 3.3)." + pollTimer.start(); + } + else { + expirationTimer.stop(); + emit serverClosed(true); + // let O2 handle the other cases + emit verificationReceived(params); + } + } + reply->deleteLater(); +} + +} diff --git a/ultimmc/libraries/katabasis/src/Reply.cpp b/ultimmc/libraries/katabasis/src/Reply.cpp new file mode 100644 index 0000000..3e27a7e --- /dev/null +++ b/ultimmc/libraries/katabasis/src/Reply.cpp @@ -0,0 +1,65 @@ +#include +#include + +#include "katabasis/Reply.h" + +namespace Katabasis { + +Reply::Reply(QNetworkReply *r, int timeOut, QObject *parent): QTimer(parent), reply(r) { + setSingleShot(true); + connect(this, &Reply::timeout, this, &Reply::onTimeOut, Qt::QueuedConnection); + start(timeOut); +} + +void Reply::onTimeOut() { + timedOut = true; + reply->abort(); +} + +// ---------------------------- + +ReplyList::~ReplyList() { + foreach (Reply *timedReply, replies_) { + delete timedReply; + } +} + +void ReplyList::add(QNetworkReply *reply, int timeOut) { + if (reply && ignoreSslErrors()) { + reply->ignoreSslErrors(); + } + add(new Reply(reply, timeOut)); +} + +void ReplyList::add(Reply *reply) { + replies_.append(reply); +} + +void ReplyList::remove(QNetworkReply *reply) { + Reply *o2Reply = find(reply); + if (o2Reply) { + o2Reply->stop(); + (void)replies_.removeOne(o2Reply); + } +} + +Reply *ReplyList::find(QNetworkReply *reply) { + foreach (Reply *timedReply, replies_) { + if (timedReply->reply == reply) { + return timedReply; + } + } + return 0; +} + +bool ReplyList::ignoreSslErrors() +{ + return ignoreSslErrors_; +} + +void ReplyList::setIgnoreSslErrors(bool ignoreSslErrors) +{ + ignoreSslErrors_ = ignoreSslErrors; +} + +} diff --git a/ultimmc/libraries/launcher/.gitignore b/ultimmc/libraries/launcher/.gitignore new file mode 100644 index 0000000..cc1c52b --- /dev/null +++ b/ultimmc/libraries/launcher/.gitignore @@ -0,0 +1,6 @@ +.idea +*.iml +out +.classpath +.idea +.project diff --git a/ultimmc/libraries/launcher/CMakeLists.txt b/ultimmc/libraries/launcher/CMakeLists.txt new file mode 100644 index 0000000..ff2a414 --- /dev/null +++ b/ultimmc/libraries/launcher/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.1) +project(launcher Java) +find_package(Java 1.7 REQUIRED COMPONENTS Development) + +include(UseJava) +set(CMAKE_JAVA_JAR_ENTRY_POINT org.multimc.EntryPoint) +set(CMAKE_JAVA_COMPILE_FLAGS -target 7 -source 7 -Xlint:deprecation -Xlint:unchecked) + +set(SRC + org/multimc/EntryPoint.java + org/multimc/Launcher.java + org/multimc/LegacyFrame.java + org/multimc/NotFoundException.java + org/multimc/ParamBucket.java + org/multimc/ParseException.java + org/multimc/Utils.java + org/multimc/onesix/OneSixLauncher.java + net/minecraft/Launcher.java +) +add_jar(NewLaunch ${SRC}) +install_jar(NewLaunch "${JARS_DEST_DIR}") diff --git a/ultimmc/libraries/launcher/net/minecraft/Launcher.java b/ultimmc/libraries/launcher/net/minecraft/Launcher.java new file mode 100644 index 0000000..b6b0a57 --- /dev/null +++ b/ultimmc/libraries/launcher/net/minecraft/Launcher.java @@ -0,0 +1,169 @@ +/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.minecraft; + +import java.util.TreeMap; +import java.util.Map; +import java.net.URL; +import java.awt.Dimension; +import java.awt.BorderLayout; +import java.awt.Graphics; +import java.applet.Applet; +import java.applet.AppletStub; +import java.net.MalformedURLException; + +public class Launcher extends Applet implements AppletStub +{ + private Applet wrappedApplet; + private URL documentBase; + private boolean active = false; + private final Map params; + + public Launcher(Applet applet, URL documentBase) + { + params = new TreeMap(); + + this.setLayout(new BorderLayout()); + this.add(applet, "Center"); + this.wrappedApplet = applet; + this.documentBase = documentBase; + } + + public void setParameter(String name, String value) + { + params.put(name, value); + } + + public void replace(Applet applet) + { + this.wrappedApplet = applet; + + applet.setStub(this); + applet.setSize(getWidth(), getHeight()); + + this.setLayout(new BorderLayout()); + this.add(applet, "Center"); + + applet.init(); + active = true; + applet.start(); + validate(); + } + + @Override + public String getParameter(String name) + { + String param = params.get(name); + if (param != null) + return param; + try + { + return super.getParameter(name); + } catch (Exception ignore){} + return null; + } + + @Override + public boolean isActive() + { + return active; + } + + @Override + public void appletResize(int width, int height) + { + wrappedApplet.resize(width, height); + } + + @Override + public void resize(int width, int height) + { + wrappedApplet.resize(width, height); + } + + @Override + public void resize(Dimension d) + { + wrappedApplet.resize(d); + } + + @Override + public void init() + { + if (wrappedApplet != null) + { + wrappedApplet.init(); + } + } + + @Override + public void start() + { + wrappedApplet.start(); + active = true; + } + + @Override + public void stop() + { + wrappedApplet.stop(); + active = false; + } + + public void destroy() + { + wrappedApplet.destroy(); + } + + @Override + public URL getCodeBase() { + try { + return new URL("http://www.minecraft.net/game/"); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public URL getDocumentBase() + { + try { + // Special case only for Classic versions + if (wrappedApplet.getClass().getCanonicalName().startsWith("com.mojang")) { + return new URL("http", "www.minecraft.net", 80, "/game/", null); + } + return new URL("http://www.minecraft.net/game/"); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public void setVisible(boolean b) + { + super.setVisible(b); + wrappedApplet.setVisible(b); + } + public void update(Graphics paramGraphics) + { + } + public void paint(Graphics paramGraphics) + { + } +} \ No newline at end of file diff --git a/ultimmc/libraries/launcher/org/multimc/EntryPoint.java b/ultimmc/libraries/launcher/org/multimc/EntryPoint.java new file mode 100644 index 0000000..0f904f5 --- /dev/null +++ b/ultimmc/libraries/launcher/org/multimc/EntryPoint.java @@ -0,0 +1,151 @@ +package org.multimc;/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import org.multimc.onesix.OneSixLauncher; + +import java.io.*; +import java.nio.charset.Charset; + +public class EntryPoint +{ + private enum Action + { + Proceed, + Launch, + Abort + } + + public static void main(String[] args) + { + EntryPoint listener = new EntryPoint(); + int retCode = listener.listen(); + if (retCode != 0) + { + System.out.println("Exiting with " + retCode); + System.exit(retCode); + } + } + + private Action parseLine(String inData) throws ParseException + { + String[] pair = inData.split(" ", 2); + + if(pair.length == 1) + { + String command = pair[0]; + if (pair[0].equals("launch")) + return Action.Launch; + + else if (pair[0].equals("abort")) + return Action.Abort; + + else throw new ParseException("Error while parsing:" + pair[0]); + } + + if(pair.length != 2) + throw new ParseException("Pair length is not 2."); + + String command = pair[0]; + String param = pair[1]; + + if(command.equals("launcher")) + { + if(param.equals("onesix")) + { + m_launcher = new OneSixLauncher(); + Utils.log("Using onesix launcher."); + Utils.log(); + return Action.Proceed; + } + else + throw new ParseException("Invalid launcher type: " + param); + } + + m_params.add(command, param); + //System.out.println(command + " : " + param); + return Action.Proceed; + } + + public int listen() + { + BufferedReader buffer; + try + { + buffer = new BufferedReader(new InputStreamReader(System.in, "UTF-8")); + } catch (UnsupportedEncodingException e) + { + System.err.println("For some reason, your java does not support UTF-8. Consider living in the current century."); + e.printStackTrace(); + return 1; + } + boolean isListening = true; + boolean isAborted = false; + // Main loop + while (isListening) + { + String inData; + try + { + // Read from the pipe one line at a time + inData = buffer.readLine(); + if (inData != null) + { + Action a = parseLine(inData); + if(a == Action.Abort) + { + isListening = false; + isAborted = true; + } + if(a == Action.Launch) + { + isListening = false; + } + } + else + { + isListening = false; + isAborted = true; + } + } + catch (IOException e) + { + System.err.println("Launcher ABORT due to IO exception:"); + e.printStackTrace(); + return 1; + } + catch (ParseException e) + { + System.err.println("Launcher ABORT due to PARSE exception:"); + e.printStackTrace(); + return 1; + } + } + if(isAborted) + { + System.err.println("Launch aborted by the launcher."); + return 1; + } + if(m_launcher != null) + { + return m_launcher.launch(m_params); + } + System.err.println("No valid launcher implementation specified."); + return 1; + } + + private ParamBucket m_params = new ParamBucket(); + private org.multimc.Launcher m_launcher; +} diff --git a/ultimmc/libraries/launcher/org/multimc/Launcher.java b/ultimmc/libraries/launcher/org/multimc/Launcher.java new file mode 100644 index 0000000..d8cb6d1 --- /dev/null +++ b/ultimmc/libraries/launcher/org/multimc/Launcher.java @@ -0,0 +1,22 @@ +/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.multimc; + +public interface Launcher +{ + abstract int launch(ParamBucket params); +} diff --git a/ultimmc/libraries/launcher/org/multimc/LegacyFrame.java b/ultimmc/libraries/launcher/org/multimc/LegacyFrame.java new file mode 100644 index 0000000..985a10e --- /dev/null +++ b/ultimmc/libraries/launcher/org/multimc/LegacyFrame.java @@ -0,0 +1,176 @@ +package org.multimc;/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import net.minecraft.Launcher; + +import javax.imageio.ImageIO; +import java.applet.Applet; +import java.awt.*; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Scanner; + +public class LegacyFrame extends Frame implements WindowListener +{ + private Launcher appletWrap = null; + public LegacyFrame(String title) + { + super ( title ); + BufferedImage image; + try { + image = ImageIO.read ( new File ( "icon.png" ) ); + setIconImage ( image ); + } catch ( IOException e ) { + e.printStackTrace(); + } + this.addWindowListener ( this ); + } + + public void start ( + Applet mcApplet, + String user, + String session, + int winSizeW, + int winSizeH, + boolean maximize, + String serverAddress, + String serverPort + ) + { + try { + appletWrap = new Launcher( mcApplet, new URL ( "http://www.minecraft.net/game" ) ); + } catch ( MalformedURLException ignored ) {} + + // Implements support for launching in to multiplayer on classic servers using a mpticket + // file generated by an external program and stored in the instance's root folder. + File mpticketFile = null; + Scanner fileReader = null; + try { + mpticketFile = new File(System.getProperty("user.dir") + "/../mpticket").getCanonicalFile(); + fileReader = new Scanner(new FileInputStream(mpticketFile), "ascii"); + String[] mpticketParams = new String[3]; + + for(int i=0;i<3;i++) { + if(fileReader.hasNextLine()) { + mpticketParams[i] = fileReader.nextLine(); + } else { + throw new IllegalArgumentException(); + } + } + + // Assumes parameters are valid and in the correct order + appletWrap.setParameter("server", mpticketParams[0]); + appletWrap.setParameter("port", mpticketParams[1]); + appletWrap.setParameter("mppass", mpticketParams[2]); + + fileReader.close(); + mpticketFile.delete(); + } + catch (FileNotFoundException e) {} + catch (IllegalArgumentException e) { + + fileReader.close(); + File mpticketFileCorrupt = new File(System.getProperty("user.dir") + "/../mpticket.corrupt"); + if(mpticketFileCorrupt.exists()) { + mpticketFileCorrupt.delete(); + } + mpticketFile.renameTo(mpticketFileCorrupt); + + System.err.println("Malformed mpticket file, missing argument."); + e.printStackTrace(System.err); + System.exit(-1); + } + catch (Exception e) { + e.printStackTrace(System.err); + System.exit(-1); + } + + if (serverAddress != null) + { + appletWrap.setParameter("server", serverAddress); + appletWrap.setParameter("port", serverPort); + } + + appletWrap.setParameter ( "username", user ); + appletWrap.setParameter ( "sessionid", session ); + appletWrap.setParameter ( "stand-alone", "true" ); // Show the quit button. + appletWrap.setParameter ( "haspaid", "true" ); // Some old versions need this for world saves to work. + appletWrap.setParameter ( "demo", "false" ); + appletWrap.setParameter ( "fullscreen", "false" ); + mcApplet.setStub(appletWrap); + this.add ( appletWrap ); + appletWrap.setPreferredSize ( new Dimension (winSizeW, winSizeH) ); + this.pack(); + this.setLocationRelativeTo ( null ); + this.setResizable ( true ); + if ( maximize ) { + this.setExtendedState ( MAXIMIZED_BOTH ); + } + validate(); + appletWrap.init(); + appletWrap.start(); + setVisible ( true ); + } + + @Override + public void windowActivated ( WindowEvent e ) {} + + @Override + public void windowClosed ( WindowEvent e ) {} + + @Override + public void windowClosing ( WindowEvent e ) + { + new Thread() { + public void run() { + try { + Thread.sleep ( 30000L ); + } catch ( InterruptedException localInterruptedException ) { + localInterruptedException.printStackTrace(); + } + System.out.println ( "FORCING EXIT!" ); + System.exit ( 0 ); + } + } + .start(); + + if ( appletWrap != null ) { + appletWrap.stop(); + appletWrap.destroy(); + } + // old minecraft versions can hang without this >_< + System.exit ( 0 ); + } + + @Override + public void windowDeactivated ( WindowEvent e ) {} + + @Override + public void windowDeiconified ( WindowEvent e ) {} + + @Override + public void windowIconified ( WindowEvent e ) {} + + @Override + public void windowOpened ( WindowEvent e ) {} +} diff --git a/ultimmc/libraries/launcher/org/multimc/NotFoundException.java b/ultimmc/libraries/launcher/org/multimc/NotFoundException.java new file mode 100644 index 0000000..ba12951 --- /dev/null +++ b/ultimmc/libraries/launcher/org/multimc/NotFoundException.java @@ -0,0 +1,21 @@ +/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.multimc; + +public class NotFoundException extends Exception +{ +} diff --git a/ultimmc/libraries/launcher/org/multimc/ParamBucket.java b/ultimmc/libraries/launcher/org/multimc/ParamBucket.java new file mode 100644 index 0000000..2fde132 --- /dev/null +++ b/ultimmc/libraries/launcher/org/multimc/ParamBucket.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.multimc; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class ParamBucket +{ + public void add(String key, String value) + { + List coll = null; + if(!m_params.containsKey(key)) + { + coll = new ArrayList(); + m_params.put(key, coll); + } + else + { + coll = m_params.get(key); + } + coll.add(value); + } + + public List all(String key) throws NotFoundException + { + if(!m_params.containsKey(key)) + throw new NotFoundException(); + return m_params.get(key); + } + + public List allSafe(String key, List def) + { + if(!m_params.containsKey(key) || m_params.get(key).size() < 1) + { + return def; + } + return m_params.get(key); + } + + public List allSafe(String key) + { + return allSafe(key, new ArrayList()); + } + + public String first(String key) throws NotFoundException + { + List list = all(key); + if(list.size() < 1) + { + throw new NotFoundException(); + } + return list.get(0); + } + + public String firstSafe(String key, String def) + { + if(!m_params.containsKey(key) || m_params.get(key).size() < 1) + { + return def; + } + return m_params.get(key).get(0); + } + + public String firstSafe(String key) + { + return firstSafe(key, ""); + } + + private HashMap> m_params = new HashMap>(); +} diff --git a/ultimmc/libraries/launcher/org/multimc/ParseException.java b/ultimmc/libraries/launcher/org/multimc/ParseException.java new file mode 100644 index 0000000..7ea44c1 --- /dev/null +++ b/ultimmc/libraries/launcher/org/multimc/ParseException.java @@ -0,0 +1,25 @@ +/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.multimc; + +public class ParseException extends java.lang.Exception +{ + public ParseException() { super(); } + public ParseException(String message) { + super(message); + } +} diff --git a/ultimmc/libraries/launcher/org/multimc/Utils.java b/ultimmc/libraries/launcher/org/multimc/Utils.java new file mode 100644 index 0000000..353af7d --- /dev/null +++ b/ultimmc/libraries/launcher/org/multimc/Utils.java @@ -0,0 +1,119 @@ +/* + * Copyright 2012-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.multimc; + +import java.io.*; +import java.io.File; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +public class Utils +{ + /** + * Combine two parts of a path. + * + * @param path1 + * @param path2 + * @return the paths, combined + */ + public static String combine(String path1, String path2) + { + File file1 = new File(path1); + File file2 = new File(file1, path2); + return file2.getPath(); + } + + /** + * Join a list of strings into a string using a separator! + * + * @param strings the string list to join + * @param separator the glue + * @return the result. + */ + public static String join(List strings, String separator) + { + StringBuilder sb = new StringBuilder(); + String sep = ""; + for (String s : strings) + { + sb.append(sep).append(s); + sep = separator; + } + return sb.toString(); + } + + /** + * Finds a field that looks like a Minecraft base folder in a supplied class + * + * @param mc the class to scan + */ + public static Field getMCPathField(Class mc) + { + Field[] fields = mc.getDeclaredFields(); + + for (Field f : fields) + { + if (f.getType() != File.class) + { + // Has to be File + continue; + } + if (f.getModifiers() != (Modifier.PRIVATE + Modifier.STATIC)) + { + // And Private Static. + continue; + } + return f; + } + return null; + } + + /** + * Log to the launcher console + * + * @param message A String containing the message + * @param level A String containing the level name. See MinecraftLauncher::getLevel() + */ + public static void log(String message, String level) + { + // Kinda dirty + String tag = "!![" + level + "]!"; + System.out.println(tag + message.replace("\n", "\n" + tag)); + } + + public static void log(String message) + { + log(message, "Launcher"); + } + + public static void log() + { + System.out.println(); + } +} + diff --git a/ultimmc/libraries/launcher/org/multimc/onesix/OneSixLauncher.java b/ultimmc/libraries/launcher/org/multimc/onesix/OneSixLauncher.java new file mode 100644 index 0000000..edf1c9b --- /dev/null +++ b/ultimmc/libraries/launcher/org/multimc/onesix/OneSixLauncher.java @@ -0,0 +1,281 @@ +/* Copyright 2012-2023 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.multimc.onesix; + +import org.multimc.*; + +import java.applet.Applet; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +public class OneSixLauncher implements Launcher +{ + // parameters, separated from ParamBucket + private List libraries; + private List mcparams; + private List mods; + private List jarmods; + private List coremods; + private List traits; + private String appletClass; + private String mainClass; + private String nativePath; + private String userName, sessionId; + private String windowTitle; + private String windowParams; + + private String instanceTitle; + private String instanceIconId; + + // secondary parameters + private int winSizeW; + private int winSizeH; + private boolean maximize; + private String cwd; + + private String serverAddress; + private String serverPort; + private boolean useQuickPlay; + + private String joinWorld; + + // the much abused system classloader, for convenience (for further abuse) + private ClassLoader cl; + + private void processParams(ParamBucket params) throws NotFoundException + { + libraries = params.all("cp"); + mcparams = params.allSafe("param", new ArrayList() ); + mainClass = params.firstSafe("mainClass", "net.minecraft.client.Minecraft"); + appletClass = params.firstSafe("appletClass", "net.minecraft.client.MinecraftApplet"); + traits = params.allSafe("traits", new ArrayList()); + nativePath = params.first("natives"); + + userName = params.first("userName"); + sessionId = params.first("sessionId"); + windowTitle = params.firstSafe("windowTitle", "Minecraft"); + windowParams = params.firstSafe("windowParams", "854x480"); + + instanceTitle = params.firstSafe("instanceTitle", "Minecraft"); + instanceIconId = params.firstSafe("instanceIconId", "default"); + + // NOTE: this is included for the CraftPresence mod + System.setProperty("multimc.instance.title", instanceTitle); + System.setProperty("multimc.instance.icon", instanceIconId); + + serverAddress = params.firstSafe("serverAddress", null); + serverPort = params.firstSafe("serverPort", null); + useQuickPlay = params.firstSafe("useQuickPlay").startsWith("1"); + joinWorld = params.firstSafe("joinWorld", null); + + cwd = System.getProperty("user.dir"); + + winSizeW = 854; + winSizeH = 480; + maximize = false; + + String[] dimStrings = windowParams.split("x"); + + if (windowParams.equalsIgnoreCase("max")) + { + maximize = true; + } + else if (dimStrings.length == 2) + { + try + { + winSizeW = Integer.parseInt(dimStrings[0]); + winSizeH = Integer.parseInt(dimStrings[1]); + } catch (NumberFormatException ignored) {} + } + } + + int legacyLaunch() + { + // Get the Minecraft Class and set the base folder + Class mc; + try + { + mc = cl.loadClass(mainClass); + + Field f = Utils.getMCPathField(mc); + + if (f == null) + { + System.err.println("Could not find Minecraft path field."); + } + else + { + f.setAccessible(true); + f.set(null, new File(cwd)); + } + } catch (Exception e) + { + System.err.println("Could not set base folder. Failed to find/access Minecraft main class:"); + e.printStackTrace(System.err); + return -1; + } + + System.setProperty("minecraft.applet.TargetDirectory", cwd); + + if(!traits.contains("noapplet")) + { + Utils.log("Launching with applet wrapper..."); + try + { + Class MCAppletClass = cl.loadClass(appletClass); + Applet mcappl = (Applet) MCAppletClass.newInstance(); + LegacyFrame mcWindow = new LegacyFrame(windowTitle); + mcWindow.start(mcappl, userName, sessionId, winSizeW, winSizeH, maximize, serverAddress, serverPort); + return 0; + } catch (Exception e) + { + Utils.log("Applet wrapper failed:", "Error"); + e.printStackTrace(System.err); + Utils.log(); + Utils.log("Falling back to using main class."); + } + } + + // init params for the main method to chomp on. + String[] paramsArray = mcparams.toArray(new String[mcparams.size()]); + try + { + Method meth = mc.getMethod("main", String[].class); + meth.setAccessible(true); + meth.invoke(null, (Object) paramsArray); + return 0; + } catch (Exception e) + { + Utils.log("Failed to invoke the Minecraft main class:", "Fatal"); + (e instanceof InvocationTargetException ? e.getCause() : e).printStackTrace(System.err); + return -1; + } + } + + int launchWithMainClass() + { + // window size, title and state, onesix + if (maximize) + { + // FIXME: there is no good way to maximize the minecraft window in onesix. + // the following often breaks linux screen setups + // mcparams.add("--fullscreen"); + } + else + { + mcparams.add("--width"); + mcparams.add(Integer.toString(winSizeW)); + mcparams.add("--height"); + mcparams.add(Integer.toString(winSizeH)); + } + + if (joinWorld != null) + { + mcparams.add("--quickPlaySingleplayer"); + mcparams.add(joinWorld); + } + + if (serverAddress != null) + { + if (useQuickPlay) + { + mcparams.add("--quickPlayMultiplayer"); + mcparams.add(serverAddress + ":" + serverPort); + } + else + { + mcparams.add("--server"); + mcparams.add(serverAddress); + mcparams.add("--port"); + mcparams.add(serverPort); + } + } + + // Get the Minecraft Class. + Class mc; + try + { + mc = cl.loadClass(mainClass); + } catch (ClassNotFoundException e) + { + System.err.println("Failed to find Minecraft main class:"); + e.printStackTrace(System.err); + return -1; + } + + // get the main method. + Method meth; + try + { + meth = mc.getMethod("main", String[].class); + meth.setAccessible(true); + } catch (NoSuchMethodException | SecurityException e) + { + System.err.println("Failed to acquire the main method:"); + e.printStackTrace(System.err); + return -1; + } + + // init params for the main method to chomp on. + String[] paramsArray = mcparams.toArray(new String[mcparams.size()]); + try + { + // static method doesn't have an instance + meth.invoke(null, (Object) paramsArray); + } catch (Exception e) + { + System.err.println("Failed to start Minecraft:"); + (e instanceof InvocationTargetException ? e.getCause() : e).printStackTrace(System.err); + return -1; + } + return 0; + } + + @Override + public int launch(ParamBucket params) + { + // get and process the launch script params + try + { + processParams(params); + } catch (NotFoundException e) + { + System.err.println("Not enough arguments."); + e.printStackTrace(System.err); + return -1; + } + + // grab the system classloader and ... + cl = ClassLoader.getSystemClassLoader(); + + if (traits.contains("legacyLaunch") || traits.contains("alphaLaunch") ) + { + // legacy launch uses the applet wrapper + return legacyLaunch(); + } + else + { + // normal launch just calls main() + return launchWithMainClass(); + } + } +} diff --git a/ultimmc/libraries/libnbtplusplus/.gitattributes b/ultimmc/libraries/libnbtplusplus/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/ultimmc/libraries/libnbtplusplus/.gitignore b/ultimmc/libraries/libnbtplusplus/.gitignore new file mode 100644 index 0000000..b1ef936 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/.gitignore @@ -0,0 +1,11 @@ +*.cbp +*.depend +*.layout +*.cbtemp +*.bak +*.swp +/bin +/lib +/obj +/build +/doxygen diff --git a/ultimmc/libraries/libnbtplusplus/CMakeLists.txt b/ultimmc/libraries/libnbtplusplus/CMakeLists.txt new file mode 100644 index 0000000..1faa5dc --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/CMakeLists.txt @@ -0,0 +1,74 @@ +cmake_minimum_required(VERSION 3.1) +project(libnbt++ + VERSION 2.3) + +# supported configure options +option(NBT_BUILD_SHARED "Build shared libraries" OFF) +option(NBT_USE_ZLIB "Build additional zlib stream functionality" ON) +option(NBT_BUILD_TESTS "Build the unit tests. Requires CxxTest." ON) + +if(NBT_NAME) + message("Using override nbt++ name: ${NBT_NAME}") +else() + set(NBT_NAME nbt++) +endif() + +# hide this from includers. +set(BUILD_SHARED_LIBS ${NBT_BUILD_SHARED}) + +include(GenerateExportHeader) + +set(NBT_SOURCES + src/endian_str.cpp + src/tag.cpp + src/tag_array.cpp + src/tag_compound.cpp + src/tag_list.cpp + src/tag_string.cpp + src/value.cpp + src/value_initializer.cpp + + src/io/stream_reader.cpp + src/io/stream_writer.cpp + + src/text/json_formatter.cpp) + +set(NBT_SOURCES_Z + src/io/izlibstream.cpp + src/io/ozlibstream.cpp) + +if(NBT_USE_ZLIB) + find_package(ZLIB REQUIRED) + list(APPEND NBT_SOURCES ${NBT_SOURCES_Z}) + add_definitions("-DNBT_HAVE_ZLIB") +endif() + +add_library(${NBT_NAME} ${NBT_SOURCES}) +target_include_directories(${NBT_NAME} PUBLIC include ${CMAKE_CURRENT_BINARY_DIR}) + +# Install it +if(DEFINED NBT_DEST_DIR) + install( + TARGETS ${NBT_NAME} + ARCHIVE DESTINATION ${LIBRARY_DEST_DIR} + RUNTIME DESTINATION ${LIBRARY_DEST_DIR} + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} + ) +endif() + +if(NBT_USE_ZLIB) + target_link_libraries(${NBT_NAME} z) +endif() +set_property(TARGET ${NBT_NAME} PROPERTY CXX_STANDARD 11) +generate_export_header(${NBT_NAME} BASE_NAME nbt) + +if(${BUILD_SHARED_LIBS}) + set_target_properties(${NBT_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN 1) +endif() + +if(NBT_BUILD_TESTS) + enable_testing() + add_subdirectory(test) +endif() diff --git a/ultimmc/libraries/libnbtplusplus/COPYING b/ultimmc/libraries/libnbtplusplus/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/ultimmc/libraries/libnbtplusplus/COPYING.LESSER b/ultimmc/libraries/libnbtplusplus/COPYING.LESSER new file mode 100644 index 0000000..65c5ca8 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/COPYING.LESSER @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/ultimmc/libraries/libnbtplusplus/README.md b/ultimmc/libraries/libnbtplusplus/README.md new file mode 100644 index 0000000..6ec6b57 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/README.md @@ -0,0 +1,12 @@ +# libnbt++ 2 + +libnbt++ is a free C++ library for Minecraft's file format Named Binary Tag +(NBT). It can read and write compressed and uncompressed NBT files and +provides a code interface for working with NBT data. + +---------- + +libnbt++2 is a remake of the old libnbt++ library with the goal of making it +more easily usable and fixing some problems. The old libnbt++ especially +suffered from a very convoluted syntax and boilerplate code needed to work +with NBT data. diff --git a/ultimmc/libraries/libnbtplusplus/include/crtp_tag.h b/ultimmc/libraries/libnbtplusplus/include/crtp_tag.h new file mode 100644 index 0000000..7b80297 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/crtp_tag.h @@ -0,0 +1,64 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef CRTP_TAG_H_INCLUDED +#define CRTP_TAG_H_INCLUDED + +#include "tag.h" +#include "nbt_visitor.h" +#include "make_unique.h" + +namespace nbt +{ + +namespace detail +{ + + template + class crtp_tag : public tag + { + public: + //Pure virtual destructor to make the class abstract + virtual ~crtp_tag() noexcept = 0; + + tag_type get_type() const noexcept override final { return Sub::type; }; + + std::unique_ptr clone() const& override final { return make_unique(sub_this()); } + std::unique_ptr move_clone() && override final { return make_unique(std::move(sub_this())); } + + tag& assign(tag&& rhs) override final { return sub_this() = dynamic_cast(rhs); } + + void accept(nbt_visitor& visitor) override final { visitor.visit(sub_this()); } + void accept(const_nbt_visitor& visitor) const override final { visitor.visit(sub_this()); } + + private: + bool equals(const tag& rhs) const override final { return sub_this() == static_cast(rhs); } + + Sub& sub_this() { return static_cast(*this); } + const Sub& sub_this() const { return static_cast(*this); } + }; + + template + crtp_tag::~crtp_tag() noexcept {} + +} + +} + +#endif // CRTP_TAG_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/endian_str.h b/ultimmc/libraries/libnbtplusplus/include/endian_str.h new file mode 100644 index 0000000..ca36835 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/endian_str.h @@ -0,0 +1,112 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef ENDIAN_STR_H_INCLUDED +#define ENDIAN_STR_H_INCLUDED + +#include +#include +#include "nbt_export.h" + +/** + * @brief Reading and writing numbers from and to streams + * in binary format with different byte orders. + */ +namespace endian +{ + +enum endian { little, big }; + +///Reads number from stream in specified endian +template +void read(std::istream& is, T& x, endian e); + +///Reads number from stream in little endian +NBT_EXPORT void read_little(std::istream& is, uint8_t& x); +NBT_EXPORT void read_little(std::istream& is, uint16_t& x); +NBT_EXPORT void read_little(std::istream& is, uint32_t& x); +NBT_EXPORT void read_little(std::istream& is, uint64_t& x); +NBT_EXPORT void read_little(std::istream& is, int8_t& x); +NBT_EXPORT void read_little(std::istream& is, int16_t& x); +NBT_EXPORT void read_little(std::istream& is, int32_t& x); +NBT_EXPORT void read_little(std::istream& is, int64_t& x); +NBT_EXPORT void read_little(std::istream& is, float& x); +NBT_EXPORT void read_little(std::istream& is, double& x); + +///Reads number from stream in big endian +NBT_EXPORT void read_big(std::istream& is, uint8_t& x); +NBT_EXPORT void read_big(std::istream& is, uint16_t& x); +NBT_EXPORT void read_big(std::istream& is, uint32_t& x); +NBT_EXPORT void read_big(std::istream& is, uint64_t& x); +NBT_EXPORT void read_big(std::istream& is, int8_t& x); +NBT_EXPORT void read_big(std::istream& is, int16_t& x); +NBT_EXPORT void read_big(std::istream& is, int32_t& x); +NBT_EXPORT void read_big(std::istream& is, int64_t& x); +NBT_EXPORT void read_big(std::istream& is, float& x); +NBT_EXPORT void read_big(std::istream& is, double& x); + +///Writes number to stream in specified endian +template +void write(std::ostream& os, T x, endian e); + +///Writes number to stream in little endian +NBT_EXPORT void write_little(std::ostream& os, uint8_t x); +NBT_EXPORT void write_little(std::ostream& os, uint16_t x); +NBT_EXPORT void write_little(std::ostream& os, uint32_t x); +NBT_EXPORT void write_little(std::ostream& os, uint64_t x); +NBT_EXPORT void write_little(std::ostream& os, int8_t x); +NBT_EXPORT void write_little(std::ostream& os, int16_t x); +NBT_EXPORT void write_little(std::ostream& os, int32_t x); +NBT_EXPORT void write_little(std::ostream& os, int64_t x); +NBT_EXPORT void write_little(std::ostream& os, float x); +NBT_EXPORT void write_little(std::ostream& os, double x); + +///Writes number to stream in big endian +NBT_EXPORT void write_big(std::ostream& os, uint8_t x); +NBT_EXPORT void write_big(std::ostream& os, uint16_t x); +NBT_EXPORT void write_big(std::ostream& os, uint32_t x); +NBT_EXPORT void write_big(std::ostream& os, uint64_t x); +NBT_EXPORT void write_big(std::ostream& os, int8_t x); +NBT_EXPORT void write_big(std::ostream& os, int16_t x); +NBT_EXPORT void write_big(std::ostream& os, int32_t x); +NBT_EXPORT void write_big(std::ostream& os, int64_t x); +NBT_EXPORT void write_big(std::ostream& os, float x); +NBT_EXPORT void write_big(std::ostream& os, double x); + +template +void read(std::istream& is, T& x, endian e) +{ + if(e == little) + read_little(is, x); + else + read_big(is, x); +} + +template +void write(std::ostream& os, T x, endian e) +{ + if(e == little) + write_little(os, x); + else + write_big(os, x); +} + +} + +#endif // ENDIAN_STR_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/io/izlibstream.h b/ultimmc/libraries/libnbtplusplus/include/io/izlibstream.h new file mode 100644 index 0000000..2b9b91a --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/io/izlibstream.h @@ -0,0 +1,93 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef IZLIBSTREAM_H_INCLUDED +#define IZLIBSTREAM_H_INCLUDED + +#include "io/zlib_streambuf.h" +#include +#include + +namespace zlib +{ + +/** + * @brief Stream buffer used by zlib::izlibstream + * @sa izlibstream + */ +class NBT_EXPORT inflate_streambuf : public zlib_streambuf +{ +public: + /** + * @param input the istream to wrap + * @param bufsize the size of the internal buffers + * @param window_bits the base two logarithm of the maximum window size that + * zlib will use. + * This parameter also determines which type of input to expect. + * The default argument will autodetect between zlib and gzip data. + * Refer to the zlib documentation of inflateInit2 for more details. + * + * @throw zlib_error if zlib encounters a problem during initialization + */ + explicit inflate_streambuf(std::istream& input, size_t bufsize = 32768, int window_bits = 32 + 15); + ~inflate_streambuf() noexcept; + + ///@return the wrapped istream + std::istream& get_istr() const { return is; } + +private: + std::istream& is; + bool stream_end; + + int_type underflow() override; +}; + +/** + * @brief An istream adapter that decompresses data using zlib + * + * This istream wraps another istream. The izlibstream will read compressed + * data from the wrapped istream and inflate (decompress) it with zlib. + * + * @note If you want to read more data from the wrapped istream after the end + * of the compressed data, then it must allow seeking. It is unavoidable for + * the izlibstream to consume more data after the compressed data. + * It will automatically attempt to seek the wrapped istream back to the point + * after the end of the compressed data. + * @sa inflate_streambuf + */ +class NBT_EXPORT izlibstream : public std::istream +{ +public: + /** + * @param input the istream to wrap + * @param bufsize the size of the internal buffers + */ + explicit izlibstream(std::istream& input, size_t bufsize = 32768): + std::istream(&buf), buf(input, bufsize) + {} + ///@return the wrapped istream + std::istream& get_istr() const { return buf.get_istr(); } + +private: + inflate_streambuf buf; +}; + +} + +#endif // IZLIBSTREAM_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/io/ozlibstream.h b/ultimmc/libraries/libnbtplusplus/include/io/ozlibstream.h new file mode 100644 index 0000000..65c97c7 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/io/ozlibstream.h @@ -0,0 +1,95 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef OZLIBSTREAM_H_INCLUDED +#define OZLIBSTREAM_H_INCLUDED + +#include "io/zlib_streambuf.h" +#include +#include + +namespace zlib +{ + +/** + * @brief Stream buffer used by zlib::ozlibstream + * @sa ozlibstream + */ +class NBT_EXPORT deflate_streambuf : public zlib_streambuf +{ +public: + /** + * @param output the ostream to wrap + * @param bufsize the size of the internal buffers + * @param level the compression level, ranges from 0 to 9, or -1 for default + * + * Refer to the zlib documentation of deflateInit2 for details about the arguments. + * + * @throw zlib_error if zlib encounters a problem during initialization + */ + explicit deflate_streambuf(std::ostream& output, size_t bufsize = 32768, int level = Z_DEFAULT_COMPRESSION, int window_bits = 15, int mem_level = 8, int strategy = Z_DEFAULT_STRATEGY); + ~deflate_streambuf() noexcept; + + ///@return the wrapped ostream + std::ostream& get_ostr() const { return os; } + + ///Finishes compression and writes all pending data to the output + void close(); +private: + std::ostream& os; + + void deflate_chunk(int flush = Z_NO_FLUSH); + + int_type overflow(int_type ch) override; + int sync() override; +}; + +/** + * @brief An ostream adapter that compresses data using zlib + * + * This ostream wraps another ostream. Data written to an ozlibstream will be + * deflated (compressed) with zlib and written to the wrapped ostream. + * + * @sa deflate_streambuf + */ +class NBT_EXPORT ozlibstream : public std::ostream +{ +public: + /** + * @param output the ostream to wrap + * @param level the compression level, ranges from 0 to 9, or -1 for default + * @param gzip if true, the output will be in gzip format rather than zlib + * @param bufsize the size of the internal buffers + */ + explicit ozlibstream(std::ostream& output, int level = Z_DEFAULT_COMPRESSION, bool gzip = false, size_t bufsize = 32768): + std::ostream(&buf), buf(output, bufsize, level, 15 + (gzip ? 16 : 0)) + {} + + ///@return the wrapped ostream + std::ostream& get_ostr() const { return buf.get_ostr(); } + + ///Finishes compression and writes all pending data to the output + void close(); +private: + deflate_streambuf buf; +}; + +} + +#endif // OZLIBSTREAM_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/io/stream_reader.h b/ultimmc/libraries/libnbtplusplus/include/io/stream_reader.h new file mode 100644 index 0000000..469e18c --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/io/stream_reader.h @@ -0,0 +1,136 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef STREAM_READER_H_INCLUDED +#define STREAM_READER_H_INCLUDED + +#include "endian_str.h" +#include "tag.h" +#include "tag_compound.h" +#include +#include +#include +#include + +namespace nbt +{ +namespace io +{ + +///Exception that gets thrown when reading is not successful +class NBT_EXPORT input_error : public std::runtime_error +{ + using std::runtime_error::runtime_error; +}; + +/** + * @brief Reads a named tag from the stream, making sure that it is a compound + * @param is the stream to read from + * @param e the byte order of the source data. The Java edition + * of Minecraft uses Big Endian, the Pocket edition uses Little Endian + * @throw input_error on failure, or if the tag in the stream is not a compound + */ +NBT_EXPORT std::pair> read_compound(std::istream& is, endian::endian e = endian::big); + +/** + * @brief Reads a named tag from the stream + * @param is the stream to read from + * @param e the byte order of the source data. The Java edition + * of Minecraft uses Big Endian, the Pocket edition uses Little Endian + * @throw input_error on failure + */ +NBT_EXPORT std::pair> read_tag(std::istream& is, endian::endian e = endian::big); + +/** + * @brief Helper class for reading NBT tags from input streams + * + * Can be reused to read multiple tags + */ +class NBT_EXPORT stream_reader +{ +public: + /** + * @param is the stream to read from + * @param e the byte order of the source data. The Java edition + * of Minecraft uses Big Endian, the Pocket edition uses Little Endian + */ + explicit stream_reader(std::istream& is, endian::endian e = endian::big) noexcept; + + ///Returns the stream + std::istream& get_istr() const; + ///Returns the byte order + endian::endian get_endian() const; + + /** + * @brief Reads a named tag from the stream, making sure that it is a compound + * @throw input_error on failure, or if the tag in the stream is not a compound + */ + std::pair> read_compound(); + + /** + * @brief Reads a named tag from the stream + * @throw input_error on failure + */ + std::pair> read_tag(); + + /** + * @brief Reads a tag of the given type without name from the stream + * @throw input_error on failure + */ + std::unique_ptr read_payload(tag_type type); + + /** + * @brief Reads a tag type from the stream + * @param allow_end whether to consider tag_type::End valid + * @throw input_error on failure + */ + tag_type read_type(bool allow_end = false); + + /** + * @brief Reads a binary number from the stream + * + * On failure, will set the failbit on the stream. + */ + template + void read_num(T& x); + + /** + * @brief Reads an NBT string from the stream + * + * An NBT string consists of two bytes indicating the length, followed by + * the characters encoded in modified UTF-8. + * @throw input_error on failure + */ + std::string read_string(); + +private: + std::istream& is; + const endian::endian endian; +}; + +template +void stream_reader::read_num(T& x) +{ + endian::read(is, x, endian); +} + +} +} + +#endif // STREAM_READER_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/io/stream_writer.h b/ultimmc/libraries/libnbtplusplus/include/io/stream_writer.h new file mode 100644 index 0000000..b10f03a --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/io/stream_writer.h @@ -0,0 +1,120 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef STREAM_WRITER_H_INCLUDED +#define STREAM_WRITER_H_INCLUDED + +#include "tag.h" +#include "endian_str.h" +#include + +namespace nbt +{ +namespace io +{ + +/* Not sure if that is even needed +///Exception that gets thrown when writing is not successful +class output_error : public std::runtime_error +{ + using std::runtime_error::runtime_error; +};*/ + +/** + * @brief Writes a named tag into the stream, including the tag type + * @param key the name of the tag + * @param t the tag + * @param os the stream to write to + * @param e the byte order of the written data. The Java edition + * of Minecraft uses Big Endian, the Pocket edition uses Little Endian + */ +NBT_EXPORT void write_tag(const std::string& key, const tag& t, std::ostream& os, endian::endian e = endian::big); + +/** + * @brief Helper class for writing NBT tags to output streams + * + * Can be reused to write multiple tags + */ +class NBT_EXPORT stream_writer +{ +public: + ///Maximum length of an NBT string (16 bit unsigned) + static constexpr size_t max_string_len = UINT16_MAX; + ///Maximum length of an NBT list or array (32 bit signed) + static constexpr uint32_t max_array_len = INT32_MAX; + + /** + * @param os the stream to write to + * @param e the byte order of the written data. The Java edition + * of Minecraft uses Big Endian, the Pocket edition uses Little Endian + */ + explicit stream_writer(std::ostream& os, endian::endian e = endian::big) noexcept: + os(os), endian(e) + {} + + ///Returns the stream + std::ostream& get_ostr() const { return os; } + ///Returns the byte order + endian::endian get_endian() const { return endian; } + + /** + * @brief Writes a named tag into the stream, including the tag type + */ + void write_tag(const std::string& key, const tag& t); + + /** + * @brief Writes the given tag's payload into the stream + */ + void write_payload(const tag& t) { t.write_payload(*this); } + + /** + * @brief Writes a tag type to the stream + */ + void write_type(tag_type tt) { write_num(static_cast(tt)); } + + /** + * @brief Writes a binary number to the stream + */ + template + void write_num(T x); + + /** + * @brief Writes an NBT string to the stream + * + * An NBT string consists of two bytes indicating the length, followed by + * the characters encoded in modified UTF-8. + * @throw std::length_error if the string is too long for NBT + */ + void write_string(const std::string& str); + +private: + std::ostream& os; + const endian::endian endian; +}; + +template +void stream_writer::write_num(T x) +{ + endian::write(os, x, endian); +} + +} +} + +#endif // STREAM_WRITER_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/io/zlib_streambuf.h b/ultimmc/libraries/libnbtplusplus/include/io/zlib_streambuf.h new file mode 100644 index 0000000..4241769 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/io/zlib_streambuf.h @@ -0,0 +1,46 @@ +#ifndef ZLIB_STREAMBUF_H_INCLUDED +#define ZLIB_STREAMBUF_H_INCLUDED + +#include +#include +#include +#include +#include "nbt_export.h" + +namespace zlib +{ + +///Exception thrown in case zlib encounters a problem +class NBT_EXPORT zlib_error : public std::runtime_error +{ +public: + const int errcode; + + zlib_error(const char* msg, int errcode): + std::runtime_error(msg + ? std::string(zError(errcode)) + ": " + msg + : zError(errcode)), + errcode(errcode) + {} +}; + +///Base class for deflate_streambuf and inflate_streambuf +class zlib_streambuf : public std::streambuf +{ +protected: + std::vector in; + std::vector out; + z_stream zstr; + + explicit zlib_streambuf(size_t bufsize): + in(bufsize), out(bufsize) + { + zstr.zalloc = Z_NULL; + zstr.zfree = Z_NULL; + zstr.opaque = Z_NULL; + } +}; + +} + +#endif // ZLIB_STREAMBUF_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/make_unique.h b/ultimmc/libraries/libnbtplusplus/include/make_unique.h new file mode 100644 index 0000000..9a92954 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/make_unique.h @@ -0,0 +1,37 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef MAKE_UNIQUE_H_INCLUDED +#define MAKE_UNIQUE_H_INCLUDED + +#include + +namespace nbt +{ + +///Creates a new object of type T and returns a std::unique_ptr to it +template +std::unique_ptr make_unique(Args&&... args) +{ + return std::unique_ptr(new T(std::forward(args)...)); +} + +} + +#endif // MAKE_UNIQUE_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/nbt_tags.h b/ultimmc/libraries/libnbtplusplus/include/nbt_tags.h new file mode 100644 index 0000000..7f557fc --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/nbt_tags.h @@ -0,0 +1,24 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include "tag_primitive.h" +#include "tag_string.h" +#include "tag_array.h" +#include "tag_list.h" +#include "tag_compound.h" diff --git a/ultimmc/libraries/libnbtplusplus/include/nbt_visitor.h b/ultimmc/libraries/libnbtplusplus/include/nbt_visitor.h new file mode 100644 index 0000000..fe2688a --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/nbt_visitor.h @@ -0,0 +1,82 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef NBT_VISITOR_H_INCLUDED +#define NBT_VISITOR_H_INCLUDED + +#include "tagfwd.h" + +namespace nbt +{ + +/** + * @brief Base class for visitors of tags + * + * Implementing the Visitor pattern + */ +class nbt_visitor +{ +public: + virtual ~nbt_visitor() noexcept = 0; //Abstract class + + virtual void visit(tag_byte&) {} + virtual void visit(tag_short&) {} + virtual void visit(tag_int&) {} + virtual void visit(tag_long&) {} + virtual void visit(tag_float&) {} + virtual void visit(tag_double&) {} + virtual void visit(tag_byte_array&) {} + virtual void visit(tag_string&) {} + virtual void visit(tag_list&) {} + virtual void visit(tag_compound&) {} + virtual void visit(tag_int_array&) {} + virtual void visit(tag_long_array&) {} +}; + +/** + * @brief Base class for visitors of constant tags + * + * Implementing the Visitor pattern + */ +class const_nbt_visitor +{ +public: + virtual ~const_nbt_visitor() noexcept = 0; //Abstract class + + virtual void visit(const tag_byte&) {} + virtual void visit(const tag_short&) {} + virtual void visit(const tag_int&) {} + virtual void visit(const tag_long&) {} + virtual void visit(const tag_float&) {} + virtual void visit(const tag_double&) {} + virtual void visit(const tag_byte_array&) {} + virtual void visit(const tag_string&) {} + virtual void visit(const tag_list&) {} + virtual void visit(const tag_compound&) {} + virtual void visit(const tag_int_array&) {} + virtual void visit(const tag_long_array&) {} +}; + +inline nbt_visitor::~nbt_visitor() noexcept {} + +inline const_nbt_visitor::~const_nbt_visitor() noexcept {} + +} + +#endif // NBT_VISITOR_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/primitive_detail.h b/ultimmc/libraries/libnbtplusplus/include/primitive_detail.h new file mode 100644 index 0000000..4715ee7 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/primitive_detail.h @@ -0,0 +1,46 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef PRIMITIVE_DETAIL_H_INCLUDED +#define PRIMITIVE_DETAIL_H_INCLUDED + +#include + +///@cond +namespace nbt +{ + +namespace detail +{ + ///Meta-struct that holds the tag_type value for a specific primitive type + template struct get_primitive_type + { static_assert(sizeof(T) != sizeof(T), "Invalid type paramter for tag_primitive, can only use types that NBT uses"); }; + + template<> struct get_primitive_type : public std::integral_constant {}; + template<> struct get_primitive_type : public std::integral_constant {}; + template<> struct get_primitive_type : public std::integral_constant {}; + template<> struct get_primitive_type : public std::integral_constant {}; + template<> struct get_primitive_type : public std::integral_constant {}; + template<> struct get_primitive_type : public std::integral_constant {}; +} + +} +///@endcond + +#endif // PRIMITIVE_DETAIL_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/tag.h b/ultimmc/libraries/libnbtplusplus/include/tag.h new file mode 100644 index 0000000..c4f1d5d --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/tag.h @@ -0,0 +1,159 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef TAG_H_INCLUDED +#define TAG_H_INCLUDED + +#include +#include +#include +#include "nbt_export.h" + +namespace nbt +{ + +///Tag type values used in the binary format +enum class tag_type : int8_t +{ + End = 0, + Byte = 1, + Short = 2, + Int = 3, + Long = 4, + Float = 5, + Double = 6, + Byte_Array = 7, + String = 8, + List = 9, + Compound = 10, + Int_Array = 11, + Long_Array = 12, + Null = -1 ///< Used to denote empty @ref value s +}; + +/** + * @brief Returns whether the given number falls within the range of valid tag types + * @param allow_end whether to consider tag_type::End (0) valid + */ +NBT_EXPORT bool is_valid_type(int type, bool allow_end = false); + +//Forward declarations +class nbt_visitor; +class const_nbt_visitor; +namespace io +{ + class stream_reader; + class stream_writer; +} + +///Base class for all NBT tag classes +class NBT_EXPORT tag +{ +public: + //Virtual destructor + virtual ~tag() noexcept {} + + ///Returns the type of the tag + virtual tag_type get_type() const noexcept = 0; + + //Polymorphic clone methods + virtual std::unique_ptr clone() const& = 0; + virtual std::unique_ptr move_clone() && = 0; + std::unique_ptr clone() &&; + + /** + * @brief Returns a reference to the tag as an instance of T + * @throw std::bad_cast if the tag is not of type T + */ + template + T& as(); + template + const T& as() const; + + /** + * @brief Move-assigns the given tag if the class is the same + * @throw std::bad_cast if @c rhs is not the same type as @c *this + */ + virtual tag& assign(tag&& rhs) = 0; + + /** + * @brief Calls the appropriate overload of @c visit() on the visitor with + * @c *this as argument + * + * Implementing the Visitor pattern + */ + virtual void accept(nbt_visitor& visitor) = 0; + virtual void accept(const_nbt_visitor& visitor) const = 0; + + /** + * @brief Reads the tag's payload from the stream + * @throw io::stream_reader::input_error on failure + */ + virtual void read_payload(io::stream_reader& reader) = 0; + + /** + * @brief Writes the tag's payload into the stream + */ + virtual void write_payload(io::stream_writer& writer) const = 0; + + /** + * @brief Default-constructs a new tag of the given type + * @throw std::invalid_argument if the type is not valid (e.g. End or Null) + */ + static std::unique_ptr create(tag_type type); + + friend NBT_EXPORT bool operator==(const tag& lhs, const tag& rhs); + friend NBT_EXPORT bool operator!=(const tag& lhs, const tag& rhs); + +private: + /** + * @brief Checks for equality to a tag of the same type + * @param rhs an instance of the same class as @c *this + */ + virtual bool equals(const tag& rhs) const = 0; +}; + +///Output operator for tag types +NBT_EXPORT std::ostream& operator<<(std::ostream& os, tag_type tt); + +/** + * @brief Output operator for tags + * + * Uses @ref text::json_formatter + * @relates tag + */ +NBT_EXPORT std::ostream& operator<<(std::ostream& os, const tag& t); + +template +T& tag::as() +{ + static_assert(std::is_base_of::value, "T must be a subclass of tag"); + return dynamic_cast(*this); +} + +template +const T& tag::as() const +{ + static_assert(std::is_base_of::value, "T must be a subclass of tag"); + return dynamic_cast(*this); +} + +} + +#endif // TAG_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/tag_array.h b/ultimmc/libraries/libnbtplusplus/include/tag_array.h new file mode 100644 index 0000000..6e6a92b --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/tag_array.h @@ -0,0 +1,136 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef TAG_ARRAY_H_INCLUDED +#define TAG_ARRAY_H_INCLUDED + +#include "crtp_tag.h" +#include +#include + +namespace nbt +{ + +///@cond +namespace detail +{ + ///Meta-struct that holds the tag_type value for a specific array type + template struct get_array_type + { static_assert(sizeof(T) != sizeof(T), "Invalid type paramter for tag_array, can only use byte or int"); }; + + template<> struct get_array_type : public std::integral_constant {}; + template<> struct get_array_type : public std::integral_constant {}; + template<> struct get_array_type : public std::integral_constant {}; +} +///@cond + +/** + * @brief Tag that contains an array of byte or int values + * + * Common class for tag_byte_array, tag_int_array and tag_long_array. + */ +template +class tag_array final : public detail::crtp_tag> +{ +public: + //Iterator types + typedef typename std::vector::iterator iterator; + typedef typename std::vector::const_iterator const_iterator; + + ///The type of the contained values + typedef T value_type; + + ///The type of the tag + static constexpr tag_type type = detail::get_array_type::value; + + ///Constructs an empty array + tag_array() {} + + ///Constructs an array with the given values + tag_array(std::initializer_list init): data(init) {} + tag_array(std::vector&& vec) noexcept: data(std::move(vec)) {} + + ///Returns a reference to the vector that contains the values + std::vector& get() { return data; } + const std::vector& get() const { return data; } + + /** + * @brief Accesses a value by index with bounds checking + * @throw std::out_of_range if the index is out of range + */ + T& at(size_t i) { return data.at(i); } + T at(size_t i) const { return data.at(i); } + + /** + * @brief Accesses a value by index + * + * No bounds checking is performed. + */ + T& operator[](size_t i) { return data[i]; } + T operator[](size_t i) const { return data[i]; } + + ///Appends a value at the end of the array + void push_back(T val) { data.push_back(val); } + + ///Removes the last element from the array + void pop_back() { data.pop_back(); } + + ///Returns the number of values in the array + size_t size() const { return data.size(); } + + ///Erases all values from the array. + void clear() { data.clear(); } + + //Iterators + iterator begin() { return data.begin(); } + iterator end() { return data.end(); } + const_iterator begin() const { return data.begin(); } + const_iterator end() const { return data.end(); } + const_iterator cbegin() const { return data.cbegin(); } + const_iterator cend() const { return data.cend(); } + + void read_payload(io::stream_reader& reader) override; + /** + * @inheritdoc + * @throw std::length_error if the array is too large for NBT + */ + void write_payload(io::stream_writer& writer) const override; + +private: + std::vector data; +}; + +template bool operator==(const tag_array& lhs, const tag_array& rhs) +{ return lhs.get() == rhs.get(); } +template bool operator!=(const tag_array& lhs, const tag_array& rhs) +{ return !(lhs == rhs); } + +//Typedefs that should be used instead of the template tag_array. +typedef tag_array tag_byte_array; +typedef tag_array tag_int_array; +typedef tag_array tag_long_array; + +//Explicit instantiations +template class NBT_EXPORT tag_array; +template class NBT_EXPORT tag_array; +template class NBT_EXPORT tag_array; + +} + +#endif // TAG_ARRAY_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/tag_compound.h b/ultimmc/libraries/libnbtplusplus/include/tag_compound.h new file mode 100644 index 0000000..3bbc1f2 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/tag_compound.h @@ -0,0 +1,142 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef TAG_COMPOUND_H_INCLUDED +#define TAG_COMPOUND_H_INCLUDED + +#include "crtp_tag.h" +#include "value_initializer.h" +#include +#include + +namespace nbt +{ + +///Tag that contains multiple unordered named tags of arbitrary types +class NBT_EXPORT tag_compound final : public detail::crtp_tag +{ + typedef std::map map_t_; + +public: + //Iterator types + typedef map_t_::iterator iterator; + typedef map_t_::const_iterator const_iterator; + + ///The type of the tag + static constexpr tag_type type = tag_type::Compound; + + ///Constructs an empty compound + tag_compound() {} + + ///Constructs a compound with the given key-value pairs + tag_compound(std::initializer_list> init); + + /** + * @brief Accesses a tag by key with bounds checking + * + * Returns a value to the tag with the specified key, or throws an + * exception if it does not exist. + * @throw std::out_of_range if given key does not exist + */ + value& at(const std::string& key); + const value& at(const std::string& key) const; + + /** + * @brief Accesses a tag by key + * + * Returns a value to the tag with the specified key. If it does not exist, + * creates a new uninitialized entry under the key. + */ + value& operator[](const std::string& key) { return tags[key]; } + + /** + * @brief Inserts or assigns a tag + * + * If the given key already exists, assigns the tag to it. + * Otherwise, it is inserted under the given key. + * @return a pair of the iterator to the value and a bool indicating + * whether the key did not exist + */ + std::pair put(const std::string& key, value_initializer&& val); + + /** + * @brief Inserts a tag if the key does not exist + * @return a pair of the iterator to the value with the key and a bool + * indicating whether the value was actually inserted + */ + std::pair insert(const std::string& key, value_initializer&& val); + + /** + * @brief Constructs and assigns or inserts a tag into the compound + * + * Constructs a new tag of type @c T with the given args and inserts + * or assigns it to the given key. + * @note Unlike std::map::emplace, this will overwrite existing values + * @return a pair of the iterator to the value and a bool indicating + * whether the key did not exist + */ + template + std::pair emplace(const std::string& key, Args&&... args); + + /** + * @brief Erases a tag from the compound + * @return true if a tag was erased + */ + bool erase(const std::string& key); + + ///Returns true if the given key exists in the compound + bool has_key(const std::string& key) const; + ///Returns true if the given key exists and the tag has the given type + bool has_key(const std::string& key, tag_type type) const; + + ///Returns the number of tags in the compound + size_t size() const { return tags.size(); } + + ///Erases all tags from the compound + void clear() { tags.clear(); } + + //Iterators + iterator begin() { return tags.begin(); } + iterator end() { return tags.end(); } + const_iterator begin() const { return tags.begin(); } + const_iterator end() const { return tags.end(); } + const_iterator cbegin() const { return tags.cbegin(); } + const_iterator cend() const { return tags.cend(); } + + void read_payload(io::stream_reader& reader) override; + void write_payload(io::stream_writer& writer) const override; + + friend bool operator==(const tag_compound& lhs, const tag_compound& rhs) + { return lhs.tags == rhs.tags; } + friend bool operator!=(const tag_compound& lhs, const tag_compound& rhs) + { return !(lhs == rhs); } + +private: + map_t_ tags; +}; + +template +std::pair tag_compound::emplace(const std::string& key, Args&&... args) +{ + return put(key, value(make_unique(std::forward(args)...))); +} + +} + +#endif // TAG_COMPOUND_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/tag_list.h b/ultimmc/libraries/libnbtplusplus/include/tag_list.h new file mode 100644 index 0000000..ecd7e89 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/tag_list.h @@ -0,0 +1,223 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef TAG_LIST_H_INCLUDED +#define TAG_LIST_H_INCLUDED + +#include "crtp_tag.h" +#include "tagfwd.h" +#include "value_initializer.h" +#include +#include + +namespace nbt +{ + +/** + * @brief Tag that contains multiple unnamed tags of the same type + * + * All the tags contained in the list have the same type, which can be queried + * with el_type(). The types of the values contained in the list should not + * be changed to mismatch the element type. + * + * If the list is empty, the type can be undetermined, in which case el_type() + * will return tag_type::Null. The type will then be set when the first tag + * is added to the list. + */ +class NBT_EXPORT tag_list final : public detail::crtp_tag +{ +public: + //Iterator types + typedef std::vector::iterator iterator; + typedef std::vector::const_iterator const_iterator; + + ///The type of the tag + static constexpr tag_type type = tag_type::List; + + /** + * @brief Constructs a list of type T with the given values + * + * Example: @code tag_list::of({3, 4, 5}) @endcode + * @param init list of values from which the elements are constructed + */ + template + static tag_list of(std::initializer_list init); + + /** + * @brief Constructs an empty list + * + * The content type is determined when the first tag is added. + */ + tag_list(): tag_list(tag_type::Null) {} + + ///Constructs an empty list with the given content type + explicit tag_list(tag_type type): el_type_(type) {} + + ///Constructs a list with the given contents + tag_list(std::initializer_list init); + tag_list(std::initializer_list init); + tag_list(std::initializer_list init); + tag_list(std::initializer_list init); + tag_list(std::initializer_list init); + tag_list(std::initializer_list init); + tag_list(std::initializer_list init); + tag_list(std::initializer_list init); + tag_list(std::initializer_list init); + tag_list(std::initializer_list init); + tag_list(std::initializer_list init); + tag_list(std::initializer_list init); + + /** + * @brief Constructs a list with the given contents + * @throw std::invalid_argument if the tags are not all of the same type + */ + tag_list(std::initializer_list init); + + /** + * @brief Accesses a tag by index with bounds checking + * + * Returns a value to the tag at the specified index, or throws an + * exception if it is out of range. + * @throw std::out_of_range if the index is out of range + */ + value& at(size_t i); + const value& at(size_t i) const; + + /** + * @brief Accesses a tag by index + * + * Returns a value to the tag at the specified index. No bounds checking + * is performed. + */ + value& operator[](size_t i) { return tags[i]; } + const value& operator[](size_t i) const { return tags[i]; } + + /** + * @brief Assigns a value at the given index + * @throw std::invalid_argument if the type of the value does not match the list's + * content type + * @throw std::out_of_range if the index is out of range + */ + void set(size_t i, value&& val); + + /** + * @brief Appends the tag to the end of the list + * @throw std::invalid_argument if the type of the tag does not match the list's + * content type + */ + void push_back(value_initializer&& val); + + /** + * @brief Constructs and appends a tag to the end of the list + * @throw std::invalid_argument if the type of the tag does not match the list's + * content type + */ + template + void emplace_back(Args&&... args); + + ///Removes the last element of the list + void pop_back() { tags.pop_back(); } + + ///Returns the content type of the list, or tag_type::Null if undetermined + tag_type el_type() const { return el_type_; } + + ///Returns the number of tags in the list + size_t size() const { return tags.size(); } + + ///Erases all tags from the list. Preserves the content type. + void clear() { tags.clear(); } + + /** + * @brief Erases all tags from the list and changes the content type. + * @param type the new content type. Can be tag_type::Null to leave it undetermined. + */ + void reset(tag_type type = tag_type::Null); + + //Iterators + iterator begin() { return tags.begin(); } + iterator end() { return tags.end(); } + const_iterator begin() const { return tags.begin(); } + const_iterator end() const { return tags.end(); } + const_iterator cbegin() const { return tags.cbegin(); } + const_iterator cend() const { return tags.cend(); } + + /** + * @inheritdoc + * In case of a list of tag_end, the content type will be undetermined. + */ + void read_payload(io::stream_reader& reader) override; + /** + * @inheritdoc + * In case of a list of undetermined content type, the written type will be tag_end. + * @throw std::length_error if the list is too long for NBT + */ + void write_payload(io::stream_writer& writer) const override; + + /** + * @brief Equality comparison for lists + * + * Lists are considered equal if their content types and the contained tags + * are equal. + */ + friend NBT_EXPORT bool operator==(const tag_list& lhs, const tag_list& rhs); + friend NBT_EXPORT bool operator!=(const tag_list& lhs, const tag_list& rhs); + +private: + std::vector tags; + tag_type el_type_; + + /** + * Internally used initialization function that initializes the list with + * tags of type T, with the constructor arguments of each T given by il. + * @param il list of values that are, one by one, given to a constructor of T + */ + template + void init(std::initializer_list il); +}; + +template +tag_list tag_list::of(std::initializer_list il) +{ + tag_list result; + result.init(il); + return result; +} + +template +void tag_list::emplace_back(Args&&... args) +{ + if(el_type_ == tag_type::Null) //set content type if undetermined + el_type_ = T::type; + else if(el_type_ != T::type) + throw std::invalid_argument("The tag type does not match the list's content type"); + tags.emplace_back(make_unique(std::forward(args)...)); +} + +template +void tag_list::init(std::initializer_list init) +{ + el_type_ = T::type; + tags.reserve(init.size()); + for(const Arg& arg: init) + tags.emplace_back(make_unique(arg)); +} + +} + +#endif // TAG_LIST_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/tag_primitive.h b/ultimmc/libraries/libnbtplusplus/include/tag_primitive.h new file mode 100644 index 0000000..e3d5111 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/tag_primitive.h @@ -0,0 +1,108 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef TAG_PRIMITIVE_H_INCLUDED +#define TAG_PRIMITIVE_H_INCLUDED + +#include "crtp_tag.h" +#include "primitive_detail.h" +#include "io/stream_reader.h" +#include "io/stream_writer.h" +#include +#include + +namespace nbt +{ + +/** + * @brief Tag that contains an integral or floating-point value + * + * Common class for tag_byte, tag_short, tag_int, tag_long, tag_float and tag_double. + */ +template +class tag_primitive final : public detail::crtp_tag> +{ +public: + ///The type of the value + typedef T value_type; + + ///The type of the tag + static constexpr tag_type type = detail::get_primitive_type::value; + + //Constructor + constexpr tag_primitive(T val = 0) noexcept: value(val) {} + + //Getters + operator T&() { return value; } + constexpr operator T() const { return value; } + constexpr T get() const { return value; } + + //Setters + tag_primitive& operator=(T val) { value = val; return *this; } + void set(T val) { value = val; } + + void read_payload(io::stream_reader& reader) override; + void write_payload(io::stream_writer& writer) const override; + +private: + T value; +}; + +template bool operator==(const tag_primitive& lhs, const tag_primitive& rhs) +{ return lhs.get() == rhs.get(); } +template bool operator!=(const tag_primitive& lhs, const tag_primitive& rhs) +{ return !(lhs == rhs); } + +//Typedefs that should be used instead of the template tag_primitive. +typedef tag_primitive tag_byte; +typedef tag_primitive tag_short; +typedef tag_primitive tag_int; +typedef tag_primitive tag_long; +typedef tag_primitive tag_float; +typedef tag_primitive tag_double; + +//Explicit instantiations +template class NBT_EXPORT tag_primitive; +template class NBT_EXPORT tag_primitive; +template class NBT_EXPORT tag_primitive; +template class NBT_EXPORT tag_primitive; +template class NBT_EXPORT tag_primitive; +template class NBT_EXPORT tag_primitive; + +template +void tag_primitive::read_payload(io::stream_reader& reader) +{ + reader.read_num(value); + if(!reader.get_istr()) + { + std::ostringstream str; + str << "Error reading tag_" << type; + throw io::input_error(str.str()); + } +} + +template +void tag_primitive::write_payload(io::stream_writer& writer) const +{ + writer.write_num(value); +} + +} + +#endif // TAG_PRIMITIVE_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/tag_string.h b/ultimmc/libraries/libnbtplusplus/include/tag_string.h new file mode 100644 index 0000000..f6c49fd --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/tag_string.h @@ -0,0 +1,72 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef TAG_STRING_H_INCLUDED +#define TAG_STRING_H_INCLUDED + +#include "crtp_tag.h" +#include + +namespace nbt +{ + +///Tag that contains a UTF-8 string +class NBT_EXPORT tag_string final : public detail::crtp_tag +{ +public: + ///The type of the tag + static constexpr tag_type type = tag_type::String; + + //Constructors + tag_string() {} + tag_string(const std::string& str): value(str) {} + tag_string(std::string&& str) noexcept: value(std::move(str)) {} + tag_string(const char* str): value(str) {} + + //Getters + operator std::string&() { return value; } + operator const std::string&() const { return value; } + const std::string& get() const { return value; } + + //Setters + tag_string& operator=(const std::string& str) { value = str; return *this; } + tag_string& operator=(std::string&& str) { value = std::move(str); return *this; } + tag_string& operator=(const char* str) { value = str; return *this; } + void set(const std::string& str) { value = str; } + void set(std::string&& str) { value = std::move(str); } + + void read_payload(io::stream_reader& reader) override; + /** + * @inheritdoc + * @throw std::length_error if the string is too long for NBT + */ + void write_payload(io::stream_writer& writer) const override; + +private: + std::string value; +}; + +inline bool operator==(const tag_string& lhs, const tag_string& rhs) +{ return lhs.get() == rhs.get(); } +inline bool operator!=(const tag_string& lhs, const tag_string& rhs) +{ return !(lhs == rhs); } + +} + +#endif // TAG_STRING_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/tagfwd.h b/ultimmc/libraries/libnbtplusplus/include/tagfwd.h new file mode 100644 index 0000000..a359885 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/tagfwd.h @@ -0,0 +1,52 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +/** @file + * @brief Provides forward declarations for all tag classes + */ +#ifndef TAGFWD_H_INCLUDED +#define TAGFWD_H_INCLUDED +#include + +namespace nbt +{ + +class tag; + +template class tag_primitive; +typedef tag_primitive tag_byte; +typedef tag_primitive tag_short; +typedef tag_primitive tag_int; +typedef tag_primitive tag_long; +typedef tag_primitive tag_float; +typedef tag_primitive tag_double; + +class tag_string; + +template class tag_array; +typedef tag_array tag_byte_array; +typedef tag_array tag_int_array; +typedef tag_array tag_long_array; + +class tag_list; +class tag_compound; + +} + +#endif // TAGFWD_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/text/json_formatter.h b/ultimmc/libraries/libnbtplusplus/include/text/json_formatter.h new file mode 100644 index 0000000..876caff --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/text/json_formatter.h @@ -0,0 +1,47 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef JSON_FORMATTER_H_INCLUDED +#define JSON_FORMATTER_H_INCLUDED + +#include "tagfwd.h" +#include +#include "nbt_export.h" + +namespace nbt +{ +namespace text +{ + +/** + * @brief Prints tags in a JSON-like syntax into a stream + * + * @todo Make it configurable and able to produce actual standard-conformant JSON + */ +class NBT_EXPORT json_formatter +{ +public: + json_formatter() {} + void print(std::ostream& os, const tag& t) const; +}; + +} +} + +#endif // JSON_FORMATTER_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/value.h b/ultimmc/libraries/libnbtplusplus/include/value.h new file mode 100644 index 0000000..fffe5cd --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/value.h @@ -0,0 +1,221 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef TAG_REF_PROXY_H_INCLUDED +#define TAG_REF_PROXY_H_INCLUDED + +#include "tag.h" +#include +#include + +namespace nbt +{ + +/** + * @brief Contains an NBT value of fixed type + * + * This class is a convenience wrapper for @c std::unique_ptr. + * A value can contain any kind of tag or no tag (nullptr) and provides + * operations for handling tags of which the type is not known at compile time. + * Assignment or the set method on a value with no tag will fill in the value. + * + * The rationale for the existance of this class is to provide a type-erasured + * means of storing tags, especially when they are contained in tag_compound + * or tag_list. The alternative would be directly using @c std::unique_ptr + * and @c tag&, which is how it was done in libnbt++1. The main drawback is that + * it becomes very cumbersome to deal with tags of unknown type. + * + * For example, in this case it would not be possible to allow a syntax like + * compound["foo"] = 42. If the key "foo" does not exist beforehand, + * the left hand side could not have any sensible value if it was of type + * @c tag&. + * Firstly, the compound tag would have to create a new tag_int there, but it + * cannot know that the new tag is going to be assigned an integer. + * Also, if the type was @c tag& and it allowed assignment of integers, that + * would mean the tag base class has assignments and conversions like this. + * Which means that all other tag classes would inherit them from the base + * class, even though it does not make any sense to allow converting a + * tag_compound into an integer. Attempts like this should be caught at + * compile time. + * + * This is why all the syntactic sugar for tags is contained in the value class + * while the tag class only contains common operations for all tag types. + */ +class NBT_EXPORT value +{ +public: + //Constructors + value() noexcept {} + explicit value(std::unique_ptr&& t) noexcept: tag_(std::move(t)) {} + explicit value(tag&& t); + + //Moving + value(value&&) noexcept = default; + value& operator=(value&&) noexcept = default; + + //Copying + explicit value(const value& rhs); + value& operator=(const value& rhs); + + /** + * @brief Assigns the given value to the tag if the type matches + * @throw std::bad_cast if the type of @c t is not the same as the type + * of this value + */ + value& operator=(tag&& t); + void set(tag&& t); + + //Conversion to tag + /** + * @brief Returns the contained tag + * + * If the value is uninitialized, the behavior is undefined. + */ + operator tag&() { return get(); } + operator const tag&() const { return get(); } + tag& get() { return *tag_; } + const tag& get() const { return *tag_; } + + /** + * @brief Returns a reference to the contained tag as an instance of T + * @throw std::bad_cast if the tag is not of type T + */ + template + T& as(); + template + const T& as() const; + + //Assignment of primitives and string + /** + * @brief Assigns the given value to the tag if the type is compatible + * @throw std::bad_cast if the value is not convertible to the tag type + * via a widening conversion + */ + value& operator=(int8_t val); + value& operator=(int16_t val); + value& operator=(int32_t val); + value& operator=(int64_t val); + value& operator=(float val); + value& operator=(double val); + + /** + * @brief Assigns the given string to the tag if it is a tag_string + * @throw std::bad_cast if the contained tag is not a tag_string + */ + value& operator=(const std::string& str); + value& operator=(std::string&& str); + + //Conversions to primitives and string + /** + * @brief Returns the contained value if the type is compatible + * @throw std::bad_cast if the tag type is not convertible to the desired + * type via a widening conversion + */ + explicit operator int8_t() const; + explicit operator int16_t() const; + explicit operator int32_t() const; + explicit operator int64_t() const; + explicit operator float() const; + explicit operator double() const; + + /** + * @brief Returns the contained string if the type is tag_string + * + * If the value is uninitialized, the behavior is undefined. + * @throw std::bad_cast if the tag type is not tag_string + */ + explicit operator const std::string&() const; + + ///Returns true if the value is not uninitialized + explicit operator bool() const { return tag_ != nullptr; } + + /** + * @brief In case of a tag_compound, accesses a tag by key with bounds checking + * + * If the value is uninitialized, the behavior is undefined. + * @throw std::bad_cast if the tag type is not tag_compound + * @throw std::out_of_range if given key does not exist + * @sa tag_compound::at + */ + value& at(const std::string& key); + const value& at(const std::string& key) const; + + /** + * @brief In case of a tag_compound, accesses a tag by key + * + * If the value is uninitialized, the behavior is undefined. + * @throw std::bad_cast if the tag type is not tag_compound + * @sa tag_compound::operator[] + */ + value& operator[](const std::string& key); + value& operator[](const char* key); //need this overload because of conflict with built-in operator[] + + /** + * @brief In case of a tag_list, accesses a tag by index with bounds checking + * + * If the value is uninitialized, the behavior is undefined. + * @throw std::bad_cast if the tag type is not tag_list + * @throw std::out_of_range if the index is out of range + * @sa tag_list::at + */ + value& at(size_t i); + const value& at(size_t i) const; + + /** + * @brief In case of a tag_list, accesses a tag by index + * + * No bounds checking is performed. If the value is uninitialized, the + * behavior is undefined. + * @throw std::bad_cast if the tag type is not tag_list + * @sa tag_list::operator[] + */ + value& operator[](size_t i); + const value& operator[](size_t i) const; + + ///Returns a reference to the underlying std::unique_ptr + std::unique_ptr& get_ptr() { return tag_; } + const std::unique_ptr& get_ptr() const { return tag_; } + ///Resets the underlying std::unique_ptr to a different value + void set_ptr(std::unique_ptr&& t) { tag_ = std::move(t); } + + ///@sa tag::get_type + tag_type get_type() const; + + friend NBT_EXPORT bool operator==(const value& lhs, const value& rhs); + friend NBT_EXPORT bool operator!=(const value& lhs, const value& rhs); + +private: + std::unique_ptr tag_; +}; + +template +T& value::as() +{ + return tag_->as(); +} + +template +const T& value::as() const +{ + return tag_->as(); +} + +} + +#endif // TAG_REF_PROXY_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/include/value_initializer.h b/ultimmc/libraries/libnbtplusplus/include/value_initializer.h new file mode 100644 index 0000000..20fd436 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/include/value_initializer.h @@ -0,0 +1,65 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#ifndef VALUE_INITIALIZER_H_INCLUDED +#define VALUE_INITIALIZER_H_INCLUDED + +#include "value.h" + +namespace nbt +{ + +/** + * @brief Helper class for implicitly constructing value objects + * + * This type is a subclass of @ref value. However the only difference to value + * is that this class has additional constructors which allow implicit + * conversion of various types to value objects. These constructors are not + * part of the value class itself because implicit conversions like this + * (especially from @c tag&& to @c value) can cause problems and ambiguities + * in some cases. + * + * value_initializer is especially useful as function parameter type, it will + * allow convenient conversion of various values to tags on function call. + * + * As value_initializer objects are in no way different than value objects, + * they can just be converted to value after construction. + */ +class NBT_EXPORT value_initializer : public value +{ +public: + value_initializer(std::unique_ptr&& t) noexcept: value(std::move(t)) {} + value_initializer(std::nullptr_t) noexcept : value(nullptr) {} + value_initializer(value&& val) noexcept : value(std::move(val)) {} + value_initializer(tag&& t) : value(std::move(t)) {} + + value_initializer(int8_t val); + value_initializer(int16_t val); + value_initializer(int32_t val); + value_initializer(int64_t val); + value_initializer(float val); + value_initializer(double val); + value_initializer(const std::string& str); + value_initializer(std::string&& str); + value_initializer(const char* str); +}; + +} + +#endif // VALUE_INITIALIZER_H_INCLUDED diff --git a/ultimmc/libraries/libnbtplusplus/src/endian_str.cpp b/ultimmc/libraries/libnbtplusplus/src/endian_str.cpp new file mode 100644 index 0000000..8d136b0 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/src/endian_str.cpp @@ -0,0 +1,284 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include "endian_str.h" +#include +#include +#include + +static_assert(CHAR_BIT == 8, "Assuming that a byte has 8 bits"); +static_assert(sizeof(float) == 4, "Assuming that a float is 4 byte long"); +static_assert(sizeof(double) == 8, "Assuming that a double is 8 byte long"); + +namespace endian +{ + +namespace //anonymous +{ + void pun_int_to_float(float& f, uint32_t i) + { + //Yes we need to do it this way to avoid undefined behavior + memcpy(&f, &i, 4); + } + + uint32_t pun_float_to_int(float f) + { + uint32_t ret; + memcpy(&ret, &f, 4); + return ret; + } + + void pun_int_to_double(double& d, uint64_t i) + { + memcpy(&d, &i, 8); + } + + uint64_t pun_double_to_int(double f) + { + uint64_t ret; + memcpy(&ret, &f, 8); + return ret; + } +} + +//------------------------------------------------------------------------------ + +void read_little(std::istream& is, uint8_t& x) +{ + is.get(reinterpret_cast(x)); +} + +void read_little(std::istream& is, uint16_t& x) +{ + uint8_t tmp[2]; + is.read(reinterpret_cast(tmp), 2); + x = uint16_t(tmp[0]) + | (uint16_t(tmp[1]) << 8); +} + +void read_little(std::istream& is, uint32_t& x) +{ + uint8_t tmp[4]; + is.read(reinterpret_cast(tmp), 4); + x = uint32_t(tmp[0]) + | (uint32_t(tmp[1]) << 8) + | (uint32_t(tmp[2]) << 16) + | (uint32_t(tmp[3]) << 24); +} + +void read_little(std::istream& is, uint64_t& x) +{ + uint8_t tmp[8]; + is.read(reinterpret_cast(tmp), 8); + x = uint64_t(tmp[0]) + | (uint64_t(tmp[1]) << 8) + | (uint64_t(tmp[2]) << 16) + | (uint64_t(tmp[3]) << 24) + | (uint64_t(tmp[4]) << 32) + | (uint64_t(tmp[5]) << 40) + | (uint64_t(tmp[6]) << 48) + | (uint64_t(tmp[7]) << 56); +} + +void read_little(std::istream& is, int8_t & x) { read_little(is, reinterpret_cast(x)); } +void read_little(std::istream& is, int16_t& x) { read_little(is, reinterpret_cast(x)); } +void read_little(std::istream& is, int32_t& x) { read_little(is, reinterpret_cast(x)); } +void read_little(std::istream& is, int64_t& x) { read_little(is, reinterpret_cast(x)); } + +void read_little(std::istream& is, float& x) +{ + uint32_t tmp; + read_little(is, tmp); + pun_int_to_float(x, tmp); +} + +void read_little(std::istream& is, double& x) +{ + uint64_t tmp; + read_little(is, tmp); + pun_int_to_double(x, tmp); +} + +//------------------------------------------------------------------------------ + +void read_big(std::istream& is, uint8_t& x) +{ + is.read(reinterpret_cast(&x), 1); +} + +void read_big(std::istream& is, uint16_t& x) +{ + uint8_t tmp[2]; + is.read(reinterpret_cast(tmp), 2); + x = uint16_t(tmp[1]) + | (uint16_t(tmp[0]) << 8); +} + +void read_big(std::istream& is, uint32_t& x) +{ + uint8_t tmp[4]; + is.read(reinterpret_cast(tmp), 4); + x = uint32_t(tmp[3]) + | (uint32_t(tmp[2]) << 8) + | (uint32_t(tmp[1]) << 16) + | (uint32_t(tmp[0]) << 24); +} + +void read_big(std::istream& is, uint64_t& x) +{ + uint8_t tmp[8]; + is.read(reinterpret_cast(tmp), 8); + x = uint64_t(tmp[7]) + | (uint64_t(tmp[6]) << 8) + | (uint64_t(tmp[5]) << 16) + | (uint64_t(tmp[4]) << 24) + | (uint64_t(tmp[3]) << 32) + | (uint64_t(tmp[2]) << 40) + | (uint64_t(tmp[1]) << 48) + | (uint64_t(tmp[0]) << 56); +} + +void read_big(std::istream& is, int8_t & x) { read_big(is, reinterpret_cast(x)); } +void read_big(std::istream& is, int16_t& x) { read_big(is, reinterpret_cast(x)); } +void read_big(std::istream& is, int32_t& x) { read_big(is, reinterpret_cast(x)); } +void read_big(std::istream& is, int64_t& x) { read_big(is, reinterpret_cast(x)); } + +void read_big(std::istream& is, float& x) +{ + uint32_t tmp; + read_big(is, tmp); + pun_int_to_float(x, tmp); +} + +void read_big(std::istream& is, double& x) +{ + uint64_t tmp; + read_big(is, tmp); + pun_int_to_double(x, tmp); +} + +//------------------------------------------------------------------------------ + +void write_little(std::ostream& os, uint8_t x) +{ + os.put(x); +} + +void write_little(std::ostream& os, uint16_t x) +{ + uint8_t tmp[2] { + uint8_t(x), + uint8_t(x >> 8)}; + os.write(reinterpret_cast(tmp), 2); +} + +void write_little(std::ostream& os, uint32_t x) +{ + uint8_t tmp[4] { + uint8_t(x), + uint8_t(x >> 8), + uint8_t(x >> 16), + uint8_t(x >> 24)}; + os.write(reinterpret_cast(tmp), 4); +} + +void write_little(std::ostream& os, uint64_t x) +{ + uint8_t tmp[8] { + uint8_t(x), + uint8_t(x >> 8), + uint8_t(x >> 16), + uint8_t(x >> 24), + uint8_t(x >> 32), + uint8_t(x >> 40), + uint8_t(x >> 48), + uint8_t(x >> 56)}; + os.write(reinterpret_cast(tmp), 8); +} + +void write_little(std::ostream& os, int8_t x) { write_little(os, static_cast(x)); } +void write_little(std::ostream& os, int16_t x) { write_little(os, static_cast(x)); } +void write_little(std::ostream& os, int32_t x) { write_little(os, static_cast(x)); } +void write_little(std::ostream& os, int64_t x) { write_little(os, static_cast(x)); } + +void write_little(std::ostream& os, float x) +{ + write_little(os, pun_float_to_int(x)); +} + +void write_little(std::ostream& os, double x) +{ + write_little(os, pun_double_to_int(x)); +} + +//------------------------------------------------------------------------------ + +void write_big(std::ostream& os, uint8_t x) +{ + os.put(x); +} + +void write_big(std::ostream& os, uint16_t x) +{ + uint8_t tmp[2] { + uint8_t(x >> 8), + uint8_t(x)}; + os.write(reinterpret_cast(tmp), 2); +} + +void write_big(std::ostream& os, uint32_t x) +{ + uint8_t tmp[4] { + uint8_t(x >> 24), + uint8_t(x >> 16), + uint8_t(x >> 8), + uint8_t(x)}; + os.write(reinterpret_cast(tmp), 4); +} + +void write_big(std::ostream& os, uint64_t x) +{ + uint8_t tmp[8] { + uint8_t(x >> 56), + uint8_t(x >> 48), + uint8_t(x >> 40), + uint8_t(x >> 32), + uint8_t(x >> 24), + uint8_t(x >> 16), + uint8_t(x >> 8), + uint8_t(x)}; + os.write(reinterpret_cast(tmp), 8); +} + +void write_big(std::ostream& os, int8_t x) { write_big(os, static_cast(x)); } +void write_big(std::ostream& os, int16_t x) { write_big(os, static_cast(x)); } +void write_big(std::ostream& os, int32_t x) { write_big(os, static_cast(x)); } +void write_big(std::ostream& os, int64_t x) { write_big(os, static_cast(x)); } + +void write_big(std::ostream& os, float x) +{ + write_big(os, pun_float_to_int(x)); +} + +void write_big(std::ostream& os, double x) +{ + write_big(os, pun_double_to_int(x)); +} + +} diff --git a/ultimmc/libraries/libnbtplusplus/src/io/izlibstream.cpp b/ultimmc/libraries/libnbtplusplus/src/io/izlibstream.cpp new file mode 100644 index 0000000..0a75124 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/src/io/izlibstream.cpp @@ -0,0 +1,98 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include "io/izlibstream.h" +#include "io/zlib_streambuf.h" + +namespace zlib +{ + +inflate_streambuf::inflate_streambuf(std::istream& input, size_t bufsize, int window_bits): + zlib_streambuf(bufsize), is(input), stream_end(false) +{ + zstr.next_in = Z_NULL; + zstr.avail_in = 0; + int ret = inflateInit2(&zstr, window_bits); + if(ret != Z_OK) + throw zlib_error(zstr.msg, ret); + + char* end = out.data() + out.size(); + setg(end, end, end); +} + +inflate_streambuf::~inflate_streambuf() noexcept +{ + inflateEnd(&zstr); +} + +inflate_streambuf::int_type inflate_streambuf::underflow() +{ + if(gptr() < egptr()) + return traits_type::to_int_type(*gptr()); + + size_t have; + do + { + //Read if input buffer is empty + if(zstr.avail_in <= 0) + { + is.read(in.data(), in.size()); + if(is.bad()) + throw std::ios_base::failure("Input stream is bad"); + size_t count = is.gcount(); + if(count == 0 && !stream_end) + throw zlib_error("Unexpected end of stream", Z_DATA_ERROR); + + zstr.next_in = reinterpret_cast(in.data()); + zstr.avail_in = count; + } + + zstr.next_out = reinterpret_cast(out.data()); + zstr.avail_out = out.size(); + + int ret = inflate(&zstr, Z_NO_FLUSH); + have = out.size() - zstr.avail_out; + switch(ret) + { + case Z_NEED_DICT: + case Z_DATA_ERROR: + throw zlib_error(zstr.msg, ret); + + case Z_MEM_ERROR: + throw std::bad_alloc(); + + case Z_STREAM_END: + if(!stream_end) + { + stream_end = true; + //In case we consumed too much, we have to rewind the input stream + is.clear(); + is.seekg(-static_cast(zstr.avail_in), std::ios_base::cur); + } + if(have == 0) + return traits_type::eof(); + break; + } + } while(have == 0); + + setg(out.data(), out.data(), out.data() + have); + return traits_type::to_int_type(*gptr()); +} + +} diff --git a/ultimmc/libraries/libnbtplusplus/src/io/ozlibstream.cpp b/ultimmc/libraries/libnbtplusplus/src/io/ozlibstream.cpp new file mode 100644 index 0000000..73f1057 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/src/io/ozlibstream.cpp @@ -0,0 +1,106 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include "io/ozlibstream.h" +#include "io/zlib_streambuf.h" + +namespace zlib +{ + +deflate_streambuf::deflate_streambuf(std::ostream& output, size_t bufsize, int level, int window_bits, int mem_level, int strategy): + zlib_streambuf(bufsize), os(output) +{ + int ret = deflateInit2(&zstr, level, Z_DEFLATED, window_bits, mem_level, strategy); + if(ret != Z_OK) + throw zlib_error(zstr.msg, ret); + + setp(in.data(), in.data() + in.size()); +} + +deflate_streambuf::~deflate_streambuf() noexcept +{ + try + { + close(); + } + catch(...) + { + //ignore as we can't do anything about it + } + deflateEnd(&zstr); +} + +void deflate_streambuf::close() +{ + deflate_chunk(Z_FINISH); +} + +void deflate_streambuf::deflate_chunk(int flush) +{ + zstr.next_in = reinterpret_cast(pbase()); + zstr.avail_in = pptr() - pbase(); + do + { + zstr.next_out = reinterpret_cast(out.data()); + zstr.avail_out = out.size(); + int ret = deflate(&zstr, flush); + if(ret != Z_OK && ret != Z_STREAM_END) + { + os.setstate(std::ios_base::failbit); + throw zlib_error(zstr.msg, ret); + } + int have = out.size() - zstr.avail_out; + if(!os.write(out.data(), have)) + throw std::ios_base::failure("Could not write to the output stream"); + } while(zstr.avail_out == 0); + setp(in.data(), in.data() + in.size()); +} + +deflate_streambuf::int_type deflate_streambuf::overflow(int_type ch) +{ + deflate_chunk(); + if(ch != traits_type::eof()) + { + *pptr() = ch; + pbump(1); + } + return ch; +} + +int deflate_streambuf::sync() +{ + deflate_chunk(); + return 0; +} + +void ozlibstream::close() +{ + try + { + buf.close(); + } + catch(...) + { + setstate(badbit); //FIXME: This will throw the wrong type of exception + //but there's no good way of setting the badbit + //without causing an exception when exceptions is set + } +} + +} diff --git a/ultimmc/libraries/libnbtplusplus/src/io/stream_reader.cpp b/ultimmc/libraries/libnbtplusplus/src/io/stream_reader.cpp new file mode 100644 index 0000000..f6f30a5 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/src/io/stream_reader.cpp @@ -0,0 +1,110 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include "io/stream_reader.h" +#include "make_unique.h" +#include "tag_compound.h" +#include + +namespace nbt +{ +namespace io +{ + +std::pair> read_compound(std::istream& is, endian::endian e) +{ + return stream_reader(is, e).read_compound(); +} + +std::pair> read_tag(std::istream& is, endian::endian e) +{ + return stream_reader(is, e).read_tag(); +} + +stream_reader::stream_reader(std::istream& is, endian::endian e) noexcept: + is(is), endian(e) +{} + +std::istream& stream_reader::get_istr() const +{ + return is; +} + +endian::endian stream_reader::get_endian() const +{ + return endian; +} + +std::pair> stream_reader::read_compound() +{ + if(read_type() != tag_type::Compound) + { + is.setstate(std::ios::failbit); + throw input_error("Tag is not a compound"); + } + std::string key = read_string(); + auto comp = make_unique(); + comp->read_payload(*this); + return {std::move(key), std::move(comp)}; +} + +std::pair> stream_reader::read_tag() +{ + tag_type type = read_type(); + std::string key = read_string(); + std::unique_ptr t = read_payload(type); + return {std::move(key), std::move(t)}; +} + +std::unique_ptr stream_reader::read_payload(tag_type type) +{ + std::unique_ptr t = tag::create(type); + t->read_payload(*this); + return t; +} + +tag_type stream_reader::read_type(bool allow_end) +{ + int type = is.get(); + if(!is) + throw input_error("Error reading tag type"); + if(!is_valid_type(type, allow_end)) + { + is.setstate(std::ios::failbit); + throw input_error("Invalid tag type: " + std::to_string(type)); + } + return static_cast(type); +} + +std::string stream_reader::read_string() +{ + uint16_t len; + read_num(len); + if(!is) + throw input_error("Error reading string"); + + std::string ret(len, '\0'); + is.read(&ret[0], len); //C++11 allows us to do this + if(!is) + throw input_error("Error reading string"); + return ret; +} + +} +} diff --git a/ultimmc/libraries/libnbtplusplus/src/io/stream_writer.cpp b/ultimmc/libraries/libnbtplusplus/src/io/stream_writer.cpp new file mode 100644 index 0000000..036c5d4 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/src/io/stream_writer.cpp @@ -0,0 +1,54 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include "io/stream_writer.h" +#include + +namespace nbt +{ +namespace io +{ + +void write_tag(const std::string& key, const tag& t, std::ostream& os, endian::endian e) +{ + stream_writer(os, e).write_tag(key, t); +} + +void stream_writer::write_tag(const std::string& key, const tag& t) +{ + write_type(t.get_type()); + write_string(key); + write_payload(t); +} + +void stream_writer::write_string(const std::string& str) +{ + if(str.size() > max_string_len) + { + os.setstate(std::ios::failbit); + std::ostringstream sstr; + sstr << "String is too long for NBT (" << str.size() << " > " << max_string_len << ")"; + throw std::length_error(sstr.str()); + } + write_num(static_cast(str.size())); + os.write(str.data(), str.size()); +} + +} +} diff --git a/ultimmc/libraries/libnbtplusplus/src/tag.cpp b/ultimmc/libraries/libnbtplusplus/src/tag.cpp new file mode 100644 index 0000000..7e3be39 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/src/tag.cpp @@ -0,0 +1,107 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include "tag.h" +#include "nbt_tags.h" +#include "text/json_formatter.h" +#include +#include +#include +#include + +namespace nbt +{ + +static_assert(std::numeric_limits::is_iec559 && std::numeric_limits::is_iec559, + "The floating point values for NBT must conform to IEC 559/IEEE 754"); + +bool is_valid_type(int type, bool allow_end) +{ + return (allow_end ? 0 : 1) <= type && type <= 12; +} + +std::unique_ptr tag::clone() && +{ + return std::move(*this).move_clone(); +} + +std::unique_ptr tag::create(tag_type type) +{ + switch(type) + { + case tag_type::Byte: return make_unique(); + case tag_type::Short: return make_unique(); + case tag_type::Int: return make_unique(); + case tag_type::Long: return make_unique(); + case tag_type::Float: return make_unique(); + case tag_type::Double: return make_unique(); + case tag_type::Byte_Array: return make_unique(); + case tag_type::String: return make_unique(); + case tag_type::List: return make_unique(); + case tag_type::Compound: return make_unique(); + case tag_type::Int_Array: return make_unique(); + case tag_type::Long_Array: return make_unique(); + + default: throw std::invalid_argument("Invalid tag type"); + } +} + +bool operator==(const tag& lhs, const tag& rhs) +{ + if(typeid(lhs) != typeid(rhs)) + return false; + return lhs.equals(rhs); +} + +bool operator!=(const tag& lhs, const tag& rhs) +{ + return !(lhs == rhs); +} + +std::ostream& operator<<(std::ostream& os, tag_type tt) +{ + switch(tt) + { + case tag_type::End: return os << "end"; + case tag_type::Byte: return os << "byte"; + case tag_type::Short: return os << "short"; + case tag_type::Int: return os << "int"; + case tag_type::Long: return os << "long"; + case tag_type::Float: return os << "float"; + case tag_type::Double: return os << "double"; + case tag_type::Byte_Array: return os << "byte_array"; + case tag_type::String: return os << "string"; + case tag_type::List: return os << "list"; + case tag_type::Compound: return os << "compound"; + case tag_type::Int_Array: return os << "int_array"; + case tag_type::Long_Array: return os << "long_array"; + case tag_type::Null: return os << "null"; + + default: return os << "invalid"; + } +} + +std::ostream& operator<<(std::ostream& os, const tag& t) +{ + static const text::json_formatter formatter; + formatter.print(os, t); + return os; +} + +} diff --git a/ultimmc/libraries/libnbtplusplus/src/tag_array.cpp b/ultimmc/libraries/libnbtplusplus/src/tag_array.cpp new file mode 100644 index 0000000..4a1668a --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/src/tag_array.cpp @@ -0,0 +1,129 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include "tag_array.h" +#include "io/stream_reader.h" +#include "io/stream_writer.h" +#include + +namespace nbt +{ + +//Slightly different between byte_array and int_array +//Reading +template<> +void tag_array::read_payload(io::stream_reader& reader) +{ + int32_t length; + reader.read_num(length); + if(length < 0) + reader.get_istr().setstate(std::ios::failbit); + if(!reader.get_istr()) + throw io::input_error("Error reading length of tag_byte_array"); + + data.resize(length); + reader.get_istr().read(reinterpret_cast(data.data()), length); + if(!reader.get_istr()) + throw io::input_error("Error reading contents of tag_byte_array"); +} + +template +void tag_array::read_payload(io::stream_reader& reader) +{ + int32_t length; + reader.read_num(length); + if(length < 0) + reader.get_istr().setstate(std::ios::failbit); + if(!reader.get_istr()) + throw io::input_error("Error reading length of generic array tag"); + + data.clear(); + data.reserve(length); + for(T i = 0; i < length; ++i) + { + T val; + reader.read_num(val); + data.push_back(val); + } + if(!reader.get_istr()) + throw io::input_error("Error reading contents of generic array tag"); +} + +template<> +void tag_array::read_payload(io::stream_reader& reader) +{ + int32_t length; + reader.read_num(length); + if(length < 0) + reader.get_istr().setstate(std::ios::failbit); + if(!reader.get_istr()) + throw io::input_error("Error reading length of tag_long_array"); + + data.clear(); + data.reserve(length); + for(int32_t i = 0; i < length; ++i) + { + int64_t val; + reader.read_num(val); + data.push_back(val); + } + if(!reader.get_istr()) + throw io::input_error("Error reading contents of tag_long_array"); +} + +//Writing +template<> +void tag_array::write_payload(io::stream_writer& writer) const +{ + if(size() > io::stream_writer::max_array_len) + { + writer.get_ostr().setstate(std::ios::failbit); + throw std::length_error("Byte array is too large for NBT"); + } + writer.write_num(static_cast(size())); + writer.get_ostr().write(reinterpret_cast(data.data()), data.size()); +} + +template +void tag_array::write_payload(io::stream_writer& writer) const +{ + if(size() > io::stream_writer::max_array_len) + { + writer.get_ostr().setstate(std::ios::failbit); + throw std::length_error("Generic array is too large for NBT"); + } + writer.write_num(static_cast(size())); + for(T i: data) + writer.write_num(i); +} + +template<> +void tag_array::write_payload(io::stream_writer& writer) const +{ + if(size() > io::stream_writer::max_array_len) + { + writer.get_ostr().setstate(std::ios::failbit); + throw std::length_error("Long array is too large for NBT"); + } + writer.write_num(static_cast(size())); + for(int64_t i: data) + writer.write_num(i); +} + +} diff --git a/ultimmc/libraries/libnbtplusplus/src/tag_compound.cpp b/ultimmc/libraries/libnbtplusplus/src/tag_compound.cpp new file mode 100644 index 0000000..4085bb4 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/src/tag_compound.cpp @@ -0,0 +1,109 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include "tag_compound.h" +#include "io/stream_reader.h" +#include "io/stream_writer.h" +#include +#include + +namespace nbt +{ + +tag_compound::tag_compound(std::initializer_list> init) +{ + for(const auto& pair: init) + tags.emplace(std::move(pair.first), std::move(pair.second)); +} + +value& tag_compound::at(const std::string& key) +{ + return tags.at(key); +} + +const value& tag_compound::at(const std::string& key) const +{ + return tags.at(key); +} + +std::pair tag_compound::put(const std::string& key, value_initializer&& val) +{ + auto it = tags.find(key); + if(it != tags.end()) + { + it->second = std::move(val); + return {it, false}; + } + else + { + return tags.emplace(key, std::move(val)); + } +} + +std::pair tag_compound::insert(const std::string& key, value_initializer&& val) +{ + return tags.emplace(key, std::move(val)); +} + +bool tag_compound::erase(const std::string& key) +{ + return tags.erase(key) != 0; +} + +bool tag_compound::has_key(const std::string& key) const +{ + return tags.find(key) != tags.end(); +} + +bool tag_compound::has_key(const std::string& key, tag_type type) const +{ + auto it = tags.find(key); + return it != tags.end() && it->second.get_type() == type; +} + +void tag_compound::read_payload(io::stream_reader& reader) +{ + clear(); + tag_type tt; + while((tt = reader.read_type(true)) != tag_type::End) + { + std::string key; + try + { + key = reader.read_string(); + } + catch(io::input_error& ex) + { + std::ostringstream str; + str << "Error reading key of tag_" << tt; + throw io::input_error(str.str()); + } + auto tptr = reader.read_payload(tt); + tags.emplace(std::move(key), value(std::move(tptr))); + } +} + +void tag_compound::write_payload(io::stream_writer& writer) const +{ + for(const auto& pair: tags) + writer.write_tag(pair.first, pair.second); + writer.write_type(tag_type::End); +} + +} diff --git a/ultimmc/libraries/libnbtplusplus/src/tag_list.cpp b/ultimmc/libraries/libnbtplusplus/src/tag_list.cpp new file mode 100644 index 0000000..1650e60 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/src/tag_list.cpp @@ -0,0 +1,151 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include "tag_list.h" +#include "nbt_tags.h" +#include "io/stream_reader.h" +#include "io/stream_writer.h" +#include + +namespace nbt +{ + +tag_list::tag_list(std::initializer_list il) { init(il); } +tag_list::tag_list(std::initializer_list il) { init(il); } +tag_list::tag_list(std::initializer_list il) { init(il); } +tag_list::tag_list(std::initializer_list il) { init(il); } +tag_list::tag_list(std::initializer_list il) { init(il); } +tag_list::tag_list(std::initializer_list il) { init(il); } +tag_list::tag_list(std::initializer_list il) { init(il); } +tag_list::tag_list(std::initializer_list il) { init(il); } +tag_list::tag_list(std::initializer_list il) { init(il); } +tag_list::tag_list(std::initializer_list il) { init(il); } +tag_list::tag_list(std::initializer_list il) { init(il); } +tag_list::tag_list(std::initializer_list il) { init(il); } + +tag_list::tag_list(std::initializer_list init) +{ + if(init.size() == 0) + el_type_ = tag_type::Null; + else + { + el_type_ = init.begin()->get_type(); + for(const value& val: init) + { + if(!val || val.get_type() != el_type_) + throw std::invalid_argument("The values are not all the same type"); + } + tags.assign(init.begin(), init.end()); + } +} + +value& tag_list::at(size_t i) +{ + return tags.at(i); +} + +const value& tag_list::at(size_t i) const +{ + return tags.at(i); +} + +void tag_list::set(size_t i, value&& val) +{ + if(val.get_type() != el_type_) + throw std::invalid_argument("The tag type does not match the list's content type"); + tags.at(i) = std::move(val); +} + +void tag_list::push_back(value_initializer&& val) +{ + if(!val) //don't allow null values + throw std::invalid_argument("The value must not be null"); + if(el_type_ == tag_type::Null) //set content type if undetermined + el_type_ = val.get_type(); + else if(el_type_ != val.get_type()) + throw std::invalid_argument("The tag type does not match the list's content type"); + tags.push_back(std::move(val)); +} + +void tag_list::reset(tag_type type) +{ + clear(); + el_type_ = type; +} + +void tag_list::read_payload(io::stream_reader& reader) +{ + tag_type lt = reader.read_type(true); + + int32_t length; + reader.read_num(length); + if(length < 0) + reader.get_istr().setstate(std::ios::failbit); + if(!reader.get_istr()) + throw io::input_error("Error reading length of tag_list"); + + if(lt != tag_type::End) + { + reset(lt); + tags.reserve(length); + + for(int32_t i = 0; i < length; ++i) + tags.emplace_back(reader.read_payload(lt)); + } + else + { + //In case of tag_end, ignore the length and leave the type undetermined + reset(tag_type::Null); + } +} + +void tag_list::write_payload(io::stream_writer& writer) const +{ + if(size() > io::stream_writer::max_array_len) + { + writer.get_ostr().setstate(std::ios::failbit); + throw std::length_error("List is too large for NBT"); + } + writer.write_type(el_type_ != tag_type::Null + ? el_type_ + : tag_type::End); + writer.write_num(static_cast(size())); + for(const auto& val: tags) + { + //check if the value is of the correct type + if(val.get_type() != el_type_) + { + writer.get_ostr().setstate(std::ios::failbit); + throw std::logic_error("The tags in the list do not all match the content type"); + } + writer.write_payload(val); + } +} + +bool operator==(const tag_list& lhs, const tag_list& rhs) +{ + return lhs.el_type_ == rhs.el_type_ && lhs.tags == rhs.tags; +} + +bool operator!=(const tag_list& lhs, const tag_list& rhs) +{ + return !(lhs == rhs); +} + +} diff --git a/ultimmc/libraries/libnbtplusplus/src/tag_string.cpp b/ultimmc/libraries/libnbtplusplus/src/tag_string.cpp new file mode 100644 index 0000000..3034781 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/src/tag_string.cpp @@ -0,0 +1,44 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include "tag_string.h" +#include "io/stream_reader.h" +#include "io/stream_writer.h" + +namespace nbt +{ + +void tag_string::read_payload(io::stream_reader& reader) +{ + try + { + value = reader.read_string(); + } + catch(io::input_error& ex) + { + throw io::input_error("Error reading tag_string"); + } +} + +void tag_string::write_payload(io::stream_writer& writer) const +{ + writer.write_string(value); +} + +} diff --git a/ultimmc/libraries/libnbtplusplus/src/text/json_formatter.cpp b/ultimmc/libraries/libnbtplusplus/src/text/json_formatter.cpp new file mode 100644 index 0000000..3001ff0 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/src/text/json_formatter.cpp @@ -0,0 +1,207 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include "text/json_formatter.h" +#include "nbt_tags.h" +#include "nbt_visitor.h" +#include +#include +#include + +namespace nbt +{ +namespace text +{ + +namespace //anonymous +{ + ///Helper class which uses the Visitor pattern to pretty-print tags + class json_fmt_visitor : public const_nbt_visitor + { + public: + json_fmt_visitor(std::ostream& os, const json_formatter& fmt): + os(os) + {} + + void visit(const tag_byte& b) override + { os << static_cast(b.get()) << "b"; } //We don't want to print a character + + void visit(const tag_short& s) override + { os << s.get() << "s"; } + + void visit(const tag_int& i) override + { os << i.get(); } + + void visit(const tag_long& l) override + { os << l.get() << "l"; } + + void visit(const tag_float& f) override + { + write_float(f.get()); + os << "f"; + } + + void visit(const tag_double& d) override + { + write_float(d.get()); + os << "d"; + } + + void visit(const tag_byte_array& ba) override + { os << "[" << ba.size() << " bytes]"; } + + void visit(const tag_string& s) override + { os << '"' << s.get() << '"'; } //TODO: escape special characters + + void visit(const tag_list& l) override + { + //Wrap lines for lists of lists or compounds. + //Lists of other types can usually be on one line without problem. + const bool break_lines = l.size() > 0 && + (l.el_type() == tag_type::List || l.el_type() == tag_type::Compound); + + os << "["; + if(break_lines) + { + os << "\n"; + ++indent_lvl; + for(unsigned int i = 0; i < l.size(); ++i) + { + indent(); + if(l[i]) + l[i].get().accept(*this); + else + write_null(); + if(i != l.size()-1) + os << ","; + os << "\n"; + } + --indent_lvl; + indent(); + } + else + { + for(unsigned int i = 0; i < l.size(); ++i) + { + if(l[i]) + l[i].get().accept(*this); + else + write_null(); + if(i != l.size()-1) + os << ", "; + } + } + os << "]"; + } + + void visit(const tag_compound& c) override + { + if(c.size() == 0) //No line breaks inside empty compounds please + { + os << "{}"; + return; + } + + os << "{\n"; + ++indent_lvl; + unsigned int i = 0; + for(const auto& kv: c) + { + indent(); + os << kv.first << ": "; + if(kv.second) + kv.second.get().accept(*this); + else + write_null(); + if(i != c.size()-1) + os << ","; + os << "\n"; + ++i; + } + --indent_lvl; + indent(); + os << "}"; + } + + void visit(const tag_int_array& ia) override + { + os << "["; + for(unsigned int i = 0; i < ia.size(); ++i) + { + os << ia[i]; + if(i != ia.size()-1) + os << ", "; + } + os << "]"; + } + + void visit(const tag_long_array& la) override + { + os << "["; + for(unsigned int i = 0; i < la.size(); ++i) + { + os << la[i]; + if(i != la.size()-1) + os << ", "; + } + os << "]"; + } + + private: + const std::string indent_str = " "; + + std::ostream& os; + int indent_lvl = 0; + + void indent() + { + for(int i = 0; i < indent_lvl; ++i) + os << indent_str; + } + + template + void write_float(T val, int precision = std::numeric_limits::max_digits10) + { + if(std::isfinite(val)) + os << std::setprecision(precision) << val; + else if(std::isinf(val)) + { + if(std::signbit(val)) + os << "-"; + os << "Infinity"; + } + else + os << "NaN"; + } + + void write_null() + { + os << "null"; + } + }; +} + +void json_formatter::print(std::ostream& os, const tag& t) const +{ + json_fmt_visitor v(os, *this); + t.accept(v); +} + +} +} diff --git a/ultimmc/libraries/libnbtplusplus/src/value.cpp b/ultimmc/libraries/libnbtplusplus/src/value.cpp new file mode 100644 index 0000000..8376dc9 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/src/value.cpp @@ -0,0 +1,376 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include "value.h" +#include "nbt_tags.h" +#include + +namespace nbt +{ + +value::value(tag&& t): + tag_(std::move(t).move_clone()) +{} + +value::value(const value& rhs): + tag_(rhs.tag_ ? rhs.tag_->clone() : nullptr) +{} + +value& value::operator=(const value& rhs) +{ + if(this != &rhs) + { + tag_ = rhs.tag_ ? rhs.tag_->clone() : nullptr; + } + return *this; +} + +value& value::operator=(tag&& t) +{ + set(std::move(t)); + return *this; +} + +void value::set(tag&& t) +{ + if(tag_) + tag_->assign(std::move(t)); + else + tag_ = std::move(t).move_clone(); +} + +//Primitive assignment +//FIXME: Make this less copypaste! +value& value::operator=(int8_t val) +{ + if(!tag_) + set(tag_byte(val)); + else switch(tag_->get_type()) + { + case tag_type::Byte: + static_cast(*tag_).set(val); + break; + case tag_type::Short: + static_cast(*tag_).set(val); + break; + case tag_type::Int: + static_cast(*tag_).set(val); + break; + case tag_type::Long: + static_cast(*tag_).set(val); + break; + case tag_type::Float: + static_cast(*tag_).set(val); + break; + case tag_type::Double: + static_cast(*tag_).set(val); + break; + + default: + throw std::bad_cast(); + } + return *this; +} + +value& value::operator=(int16_t val) +{ + if(!tag_) + set(tag_short(val)); + else switch(tag_->get_type()) + { + case tag_type::Short: + static_cast(*tag_).set(val); + break; + case tag_type::Int: + static_cast(*tag_).set(val); + break; + case tag_type::Long: + static_cast(*tag_).set(val); + break; + case tag_type::Float: + static_cast(*tag_).set(val); + break; + case tag_type::Double: + static_cast(*tag_).set(val); + break; + + default: + throw std::bad_cast(); + } + return *this; +} + +value& value::operator=(int32_t val) +{ + if(!tag_) + set(tag_int(val)); + else switch(tag_->get_type()) + { + case tag_type::Int: + static_cast(*tag_).set(val); + break; + case tag_type::Long: + static_cast(*tag_).set(val); + break; + case tag_type::Float: + static_cast(*tag_).set(val); + break; + case tag_type::Double: + static_cast(*tag_).set(val); + break; + + default: + throw std::bad_cast(); + } + return *this; +} + +value& value::operator=(int64_t val) +{ + if(!tag_) + set(tag_long(val)); + else switch(tag_->get_type()) + { + case tag_type::Long: + static_cast(*tag_).set(val); + break; + case tag_type::Float: + static_cast(*tag_).set(val); + break; + case tag_type::Double: + static_cast(*tag_).set(val); + break; + + default: + throw std::bad_cast(); + } + return *this; +} + +value& value::operator=(float val) +{ + if(!tag_) + set(tag_float(val)); + else switch(tag_->get_type()) + { + case tag_type::Float: + static_cast(*tag_).set(val); + break; + case tag_type::Double: + static_cast(*tag_).set(val); + break; + + default: + throw std::bad_cast(); + } + return *this; +} + +value& value::operator=(double val) +{ + if(!tag_) + set(tag_double(val)); + else switch(tag_->get_type()) + { + case tag_type::Double: + static_cast(*tag_).set(val); + break; + + default: + throw std::bad_cast(); + } + return *this; +} + +//Primitive conversion +value::operator int8_t() const +{ + switch(tag_->get_type()) + { + case tag_type::Byte: + return static_cast(*tag_).get(); + + default: + throw std::bad_cast(); + } +} + +value::operator int16_t() const +{ + switch(tag_->get_type()) + { + case tag_type::Byte: + return static_cast(*tag_).get(); + case tag_type::Short: + return static_cast(*tag_).get(); + + default: + throw std::bad_cast(); + } +} + +value::operator int32_t() const +{ + switch(tag_->get_type()) + { + case tag_type::Byte: + return static_cast(*tag_).get(); + case tag_type::Short: + return static_cast(*tag_).get(); + case tag_type::Int: + return static_cast(*tag_).get(); + + default: + throw std::bad_cast(); + } +} + +value::operator int64_t() const +{ + switch(tag_->get_type()) + { + case tag_type::Byte: + return static_cast(*tag_).get(); + case tag_type::Short: + return static_cast(*tag_).get(); + case tag_type::Int: + return static_cast(*tag_).get(); + case tag_type::Long: + return static_cast(*tag_).get(); + + default: + throw std::bad_cast(); + } +} + +value::operator float() const +{ + switch(tag_->get_type()) + { + case tag_type::Byte: + return static_cast(*tag_).get(); + case tag_type::Short: + return static_cast(*tag_).get(); + case tag_type::Int: + return static_cast(*tag_).get(); + case tag_type::Long: + return static_cast(*tag_).get(); + case tag_type::Float: + return static_cast(*tag_).get(); + + default: + throw std::bad_cast(); + } +} + +value::operator double() const +{ + switch(tag_->get_type()) + { + case tag_type::Byte: + return static_cast(*tag_).get(); + case tag_type::Short: + return static_cast(*tag_).get(); + case tag_type::Int: + return static_cast(*tag_).get(); + case tag_type::Long: + return static_cast(*tag_).get(); + case tag_type::Float: + return static_cast(*tag_).get(); + case tag_type::Double: + return static_cast(*tag_).get(); + + default: + throw std::bad_cast(); + } +} + +value& value::operator=(std::string&& str) +{ + if(!tag_) + set(tag_string(std::move(str))); + else + dynamic_cast(*tag_).set(std::move(str)); + return *this; +} + +value::operator const std::string&() const +{ + return dynamic_cast(*tag_).get(); +} + +value& value::at(const std::string& key) +{ + return dynamic_cast(*tag_).at(key); +} + +const value& value::at(const std::string& key) const +{ + return dynamic_cast(*tag_).at(key); +} + +value& value::operator[](const std::string& key) +{ + return dynamic_cast(*tag_)[key]; +} + +value& value::operator[](const char* key) +{ + return (*this)[std::string(key)]; +} + +value& value::at(size_t i) +{ + return dynamic_cast(*tag_).at(i); +} + +const value& value::at(size_t i) const +{ + return dynamic_cast(*tag_).at(i); +} + +value& value::operator[](size_t i) +{ + return dynamic_cast(*tag_)[i]; +} + +const value& value::operator[](size_t i) const +{ + return dynamic_cast(*tag_)[i]; +} + +tag_type value::get_type() const +{ + return tag_ ? tag_->get_type() : tag_type::Null; +} + +bool operator==(const value& lhs, const value& rhs) +{ + if(lhs.tag_ != nullptr && rhs.tag_ != nullptr) + return *lhs.tag_ == *rhs.tag_; + else + return lhs.tag_ == nullptr && rhs.tag_ == nullptr; +} + +bool operator!=(const value& lhs, const value& rhs) +{ + return !(lhs == rhs); +} + +} diff --git a/ultimmc/libraries/libnbtplusplus/src/value_initializer.cpp b/ultimmc/libraries/libnbtplusplus/src/value_initializer.cpp new file mode 100644 index 0000000..3735bfd --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/src/value_initializer.cpp @@ -0,0 +1,36 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include "value_initializer.h" +#include "nbt_tags.h" + +namespace nbt +{ + +value_initializer::value_initializer(int8_t val) : value(tag_byte(val)) {} +value_initializer::value_initializer(int16_t val) : value(tag_short(val)) {} +value_initializer::value_initializer(int32_t val) : value(tag_int(val)) {} +value_initializer::value_initializer(int64_t val) : value(tag_long(val)) {} +value_initializer::value_initializer(float val) : value(tag_float(val)) {} +value_initializer::value_initializer(double val) : value(tag_double(val)) {} +value_initializer::value_initializer(const std::string& str): value(tag_string(str)) {} +value_initializer::value_initializer(std::string&& str) : value(tag_string(std::move(str))) {} +value_initializer::value_initializer(const char* str) : value(tag_string(str)) {} + +} diff --git a/ultimmc/libraries/libnbtplusplus/test/CMakeLists.txt b/ultimmc/libraries/libnbtplusplus/test/CMakeLists.txt new file mode 100644 index 0000000..5f6e241 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/test/CMakeLists.txt @@ -0,0 +1,105 @@ +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + if(CMAKE_SYSTEM_PROCESSOR STREQUAL x86_64 OR CMAKE_SYSTEM_PROCESSOR STREQUAL amd64) + set(OBJCOPY_TARGET "elf64-x86-64") + set(OBJCOPY_ARCH "x86_64") + elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL i686) + set(OBJCOPY_TARGET "elf32-i386") + set(OBJCOPY_ARCH "i386") + else() + message(AUTHOR_WARNING "This is not a platform that would support testing nbt++") + return() + endif() +else() + message(AUTHOR_WARNING "This is not a platform that would support testing nbt++") + return() +endif() + +enable_testing() +find_package(CxxTest REQUIRED) + +include_directories(${libnbt++_SOURCE_DIR}/include) +include_directories(${CXXTEST_INCLUDE_DIR}) + +function(build_data out_var) + set(result) + foreach(in_f ${ARGN}) + set(out_f "${CMAKE_CURRENT_BINARY_DIR}/testfiles/${in_f}.obj") + add_custom_command( + COMMAND mkdir -p "${CMAKE_CURRENT_BINARY_DIR}/testfiles" + COMMAND ${CMAKE_OBJCOPY} --prefix-symbol=_ --input-target=binary --output-target=${OBJCOPY_TARGET} "${in_f}" "${out_f}" + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/testfiles/${in_f} + OUTPUT ${out_f} + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/testfiles/ + VERBATIM + ) + SET_SOURCE_FILES_PROPERTIES( + ${out_f} + PROPERTIES + EXTERNAL_OBJECT true + GENERATED true + ) + list(APPEND result ${out_f}) + endforeach() + set(${out_var} "${result}" PARENT_SCOPE) +endfunction() + +build_data(DATA_OBJECTS + bigtest.nbt + bigtest.zlib + bigtest_corrupt.nbt + bigtest_eof.nbt + bigtest_uncompr + errortest_eof1 + errortest_eof2 + errortest_neg_length + errortest_noend + littletest_uncompr + toplevel_string + trailing_data.zlib +) +add_library(NbtTestData STATIC ${DATA_OBJECTS}) + +#Specifies that the directory containing the testfiles get copied when the target is built +function(use_testfiles target) + add_custom_command(TARGET ${target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E + copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/testfiles ${CMAKE_CURRENT_BINARY_DIR}) +endfunction() + +function(stop_warnings target) + target_compile_options(${target} PRIVATE + -Wno-unused-value + -Wno-self-assign-overloaded + ) +endfunction() + +if(NBT_USE_ZLIB) + set(EXTRA_TEST_LIBS ${ZLIB_LIBRARY}) +endif() + +CXXTEST_ADD_TEST(nbttest nbttest.cpp ${CMAKE_CURRENT_SOURCE_DIR}/nbttest.h) +target_link_libraries(nbttest ${NBT_NAME}) +stop_warnings(nbttest) + +CXXTEST_ADD_TEST(endian_str_test endian_str_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/endian_str_test.h) +target_link_libraries(endian_str_test ${NBT_NAME}) +stop_warnings(endian_str_test) + +CXXTEST_ADD_TEST(read_test read_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/read_test.h) +target_link_libraries(read_test ${NBT_NAME} ${EXTRA_TEST_LIBS} NbtTestData) +stop_warnings(read_test) + +CXXTEST_ADD_TEST(write_test write_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/write_test.h) +target_link_libraries(write_test ${NBT_NAME} ${EXTRA_TEST_LIBS} NbtTestData) +stop_warnings(write_test) + +if(NBT_USE_ZLIB) + CXXTEST_ADD_TEST(zlibstream_test zlibstream_test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/zlibstream_test.h) + target_link_libraries(zlibstream_test ${NBT_NAME} ${EXTRA_TEST_LIBS} NbtTestData) + stop_warnings(zlibstream_test) +endif() + +add_executable(format_test format_test.cpp) +target_link_libraries(format_test ${NBT_NAME}) +add_test(format_test format_test) +stop_warnings(format_test) diff --git a/ultimmc/libraries/libnbtplusplus/test/data.h b/ultimmc/libraries/libnbtplusplus/test/data.h new file mode 100644 index 0000000..b699519 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/test/data.h @@ -0,0 +1,39 @@ +#pragma once + +#include + +extern "C" uint8_t __binary_bigtest_uncompr_start[]; +extern "C" uint8_t __binary_bigtest_uncompr_end[]; + +extern "C" uint8_t __binary_littletest_uncompr_start[]; +extern "C" uint8_t __binary_littletest_uncompr_end[]; + +extern "C" uint8_t __binary_errortest_eof1_start[]; +extern "C" uint8_t __binary_errortest_eof1_end[]; + +extern "C" uint8_t __binary_errortest_eof2_start[]; +extern "C" uint8_t __binary_errortest_eof2_end[]; + +extern "C" uint8_t __binary_errortest_noend_start[]; +extern "C" uint8_t __binary_errortest_noend_end[]; + +extern "C" uint8_t __binary_errortest_neg_length_start[]; +extern "C" uint8_t __binary_errortest_neg_length_end[]; + +extern "C" uint8_t __binary_toplevel_string_start[]; +extern "C" uint8_t __binary_toplevel_string_end[]; + +extern "C" uint8_t __binary_bigtest_nbt_start[]; +extern "C" uint8_t __binary_bigtest_nbt_end[]; + +extern "C" uint8_t __binary_bigtest_zlib_start[]; +extern "C" uint8_t __binary_bigtest_zlib_end[]; + +extern "C" uint8_t __binary_bigtest_corrupt_nbt_start[]; +extern "C" uint8_t __binary_bigtest_corrupt_nbt_end[]; + +extern "C" uint8_t __binary_bigtest_eof_nbt_start[]; +extern "C" uint8_t __binary_bigtest_eof_nbt_end[]; + +extern "C" uint8_t __binary_trailing_data_zlib_start[]; +extern "C" uint8_t __binary_trailing_data_zlib_end[]; diff --git a/ultimmc/libraries/libnbtplusplus/test/endian_str_test.h b/ultimmc/libraries/libnbtplusplus/test/endian_str_test.h new file mode 100644 index 0000000..6dfba9f --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/test/endian_str_test.h @@ -0,0 +1,175 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include +#include "endian_str.h" +#include +#include +#include + +using namespace endian; + +class endian_str_test : public CxxTest::TestSuite +{ +public: + void test_uint() + { + std::stringstream str(std::ios::in | std::ios::out | std::ios::binary); + + write_little(str, uint8_t (0x01)); + write_little(str, uint16_t(0x0102)); + write (str, uint32_t(0x01020304), little); + write_little(str, uint64_t(0x0102030405060708)); + + write_big (str, uint8_t (0x09)); + write_big (str, uint16_t(0x090A)); + write_big (str, uint32_t(0x090A0B0C)); + write (str, uint64_t(0x090A0B0C0D0E0F10), big); + + std::string expected{ + 1, + 2, 1, + 4, 3, 2, 1, + 8, 7, 6, 5, 4, 3, 2, 1, + + 9, + 9, 10, + 9, 10, 11, 12, + 9, 10, 11, 12, 13, 14, 15, 16 + }; + TS_ASSERT_EQUALS(str.str(), expected); + + uint8_t u8; + uint16_t u16; + uint32_t u32; + uint64_t u64; + + read_little(str, u8); + TS_ASSERT_EQUALS(u8, 0x01); + read_little(str, u16); + TS_ASSERT_EQUALS(u16, 0x0102); + read_little(str, u32); + TS_ASSERT_EQUALS(u32, 0x01020304u); + read(str, u64, little); + TS_ASSERT_EQUALS(u64, 0x0102030405060708u); + + read_big(str, u8); + TS_ASSERT_EQUALS(u8, 0x09); + read_big(str, u16); + TS_ASSERT_EQUALS(u16, 0x090A); + read(str, u32, big); + TS_ASSERT_EQUALS(u32, 0x090A0B0Cu); + read_big(str, u64); + TS_ASSERT_EQUALS(u64, 0x090A0B0C0D0E0F10u); + + TS_ASSERT(str); //Check if stream has failed + } + + void test_sint() + { + std::stringstream str(std::ios::in | std::ios::out | std::ios::binary); + + write_little(str, int8_t (-0x01)); + write_little(str, int16_t(-0x0102)); + write_little(str, int32_t(-0x01020304)); + write (str, int64_t(-0x0102030405060708), little); + + write_big (str, int8_t (-0x09)); + write_big (str, int16_t(-0x090A)); + write (str, int32_t(-0x090A0B0C), big); + write_big (str, int64_t(-0x090A0B0C0D0E0F10)); + + std::string expected{ //meh, stupid narrowing conversions + '\xFF', + '\xFE', '\xFE', + '\xFC', '\xFC', '\xFD', '\xFE', + '\xF8', '\xF8', '\xF9', '\xFA', '\xFB', '\xFC', '\xFD', '\xFE', + + '\xF7', + '\xF6', '\xF6', + '\xF6', '\xF5', '\xF4', '\xF4', + '\xF6', '\xF5', '\xF4', '\xF3', '\xF2', '\xF1', '\xF0', '\xF0' + }; + TS_ASSERT_EQUALS(str.str(), expected); + + int8_t i8; + int16_t i16; + int32_t i32; + int64_t i64; + + read_little(str, i8); + TS_ASSERT_EQUALS(i8, -0x01); + read_little(str, i16); + TS_ASSERT_EQUALS(i16, -0x0102); + read(str, i32, little); + TS_ASSERT_EQUALS(i32, -0x01020304); + read_little(str, i64); + TS_ASSERT_EQUALS(i64, -0x0102030405060708); + + read_big(str, i8); + TS_ASSERT_EQUALS(i8, -0x09); + read_big(str, i16); + TS_ASSERT_EQUALS(i16, -0x090A); + read_big(str, i32); + TS_ASSERT_EQUALS(i32, -0x090A0B0C); + read(str, i64, big); + TS_ASSERT_EQUALS(i64, -0x090A0B0C0D0E0F10); + + TS_ASSERT(str); //Check if stream has failed + } + + void test_float() + { + std::stringstream str(std::ios::in | std::ios::out | std::ios::binary); + + //C99 has hexadecimal floating point literals, C++ doesn't... + const float fconst = std::stof("-0xCDEF01p-63"); //-1.46325e-012 + const double dconst = std::stod("-0x1DEF0102030405p-375"); //-1.09484e-097 + //We will be assuming IEEE 754 here + + write_little(str, fconst); + write_little(str, dconst); + write_big (str, fconst); + write_big (str, dconst); + + std::string expected{ + '\x01', '\xEF', '\xCD', '\xAB', + '\x05', '\x04', '\x03', '\x02', '\x01', '\xEF', '\xCD', '\xAB', + + '\xAB', '\xCD', '\xEF', '\x01', + '\xAB', '\xCD', '\xEF', '\x01', '\x02', '\x03', '\x04', '\x05' + }; + TS_ASSERT_EQUALS(str.str(), expected); + + float f; + double d; + + read_little(str, f); + TS_ASSERT_EQUALS(f, fconst); + read_little(str, d); + TS_ASSERT_EQUALS(d, dconst); + + read_big(str, f); + TS_ASSERT_EQUALS(f, fconst); + read_big(str, d); + TS_ASSERT_EQUALS(d, dconst); + + TS_ASSERT(str); //Check if stream has failed + } +}; diff --git a/ultimmc/libraries/libnbtplusplus/test/format_test.cpp b/ultimmc/libraries/libnbtplusplus/test/format_test.cpp new file mode 100644 index 0000000..87f7b21 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/test/format_test.cpp @@ -0,0 +1,82 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +//#include "text/json_formatter.h" +//#include "io/stream_reader.h" +#include +#include +#include +#include "nbt_tags.h" + +using namespace nbt; + +int main() +{ + //TODO: Write that into a file + tag_compound comp{ + {"byte", tag_byte(-128)}, + {"short", tag_short(-32768)}, + {"int", tag_int(-2147483648)}, + {"long", tag_long(-9223372036854775808U)}, + + {"float 1", 1.618034f}, + {"float 2", 6.626070e-34f}, + {"float 3", 2.273737e+29f}, + {"float 4", -std::numeric_limits::infinity()}, + {"float 5", std::numeric_limits::quiet_NaN()}, + + {"double 1", 3.141592653589793}, + {"double 2", 1.749899444387479e-193}, + {"double 3", 2.850825855152578e+175}, + {"double 4", -std::numeric_limits::infinity()}, + {"double 5", std::numeric_limits::quiet_NaN()}, + + {"string 1", "Hello World! \u00E4\u00F6\u00FC\u00DF"}, + {"string 2", "String with\nline breaks\tand tabs"}, + + {"byte array", tag_byte_array{12, 13, 14, 15, 16}}, + {"int array", tag_int_array{0x0badc0de, -0x0dedbeef, 0x1badbabe}}, + {"long array", tag_long_array{0x0badc0de0badc0de, -0x0dedbeef0dedbeef, 0x1badbabe1badbabe}}, + + {"list (empty)", tag_list::of({})}, + {"list (float)", tag_list{2.0f, 1.0f, 0.5f, 0.25f}}, + {"list (list)", tag_list::of({ + {}, + {4, 5, 6}, + {tag_compound{{"egg", "ham"}}, tag_compound{{"foo", "bar"}}} + })}, + {"list (compound)", tag_list::of({ + {{"created-on", 42}, {"names", tag_list{"Compound", "tag", "#0"}}}, + {{"created-on", 45}, {"names", tag_list{"Compound", "tag", "#1"}}} + })}, + + {"compound (empty)", tag_compound()}, + {"compound (nested)", tag_compound{ + {"key", "value"}, + {"key with \u00E4\u00F6\u00FC", tag_byte(-1)}, + {"key with\nnewline and\ttab", tag_compound{}} + }}, + + {"null", nullptr} + }; + + std::cout << "----- default operator<<:\n"; + std::cout << comp; + std::cout << "\n-----" << std::endl; +} diff --git a/ultimmc/libraries/libnbtplusplus/test/nbttest.h b/ultimmc/libraries/libnbtplusplus/test/nbttest.h new file mode 100644 index 0000000..e3e16c5 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/test/nbttest.h @@ -0,0 +1,498 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include +#include "nbt_tags.h" +#include "nbt_visitor.h" +#include +#include +#include + +using namespace nbt; + +class nbttest : public CxxTest::TestSuite +{ +public: + void test_tag() + { + TS_ASSERT(!is_valid_type(-1)); + TS_ASSERT(!is_valid_type(0)); + TS_ASSERT(is_valid_type(0, true)); + TS_ASSERT(is_valid_type(1)); + TS_ASSERT(is_valid_type(5, false)); + TS_ASSERT(is_valid_type(7, true)); + TS_ASSERT(is_valid_type(12)); + TS_ASSERT(!is_valid_type(13)); + + //looks like TS_ASSERT_EQUALS can't handle abstract classes... + TS_ASSERT(*tag::create(tag_type::Byte) == tag_byte()); + TS_ASSERT_THROWS(tag::create(tag_type::Null), std::invalid_argument); + TS_ASSERT_THROWS(tag::create(tag_type::End), std::invalid_argument); + + tag_string tstr("foo"); + auto cl = tstr.clone(); + TS_ASSERT_EQUALS(tstr.get(), "foo"); + TS_ASSERT(tstr == *cl); + + cl = std::move(tstr).clone(); + TS_ASSERT(*cl == tag_string("foo")); + TS_ASSERT(*cl != tag_string("bar")); + + cl = std::move(*cl).move_clone(); + TS_ASSERT(*cl == tag_string("foo")); + + tstr.assign(tag_string("bar")); + TS_ASSERT_THROWS(tstr.assign(tag_int(6)), std::bad_cast); + TS_ASSERT_EQUALS(tstr.get(), "bar"); + + TS_ASSERT_EQUALS(&tstr.as(), &tstr); + TS_ASSERT_THROWS(tstr.as(), std::bad_cast); + } + + void test_get_type() + { + TS_ASSERT_EQUALS(tag_byte().get_type() , tag_type::Byte); + TS_ASSERT_EQUALS(tag_short().get_type() , tag_type::Short); + TS_ASSERT_EQUALS(tag_int().get_type() , tag_type::Int); + TS_ASSERT_EQUALS(tag_long().get_type() , tag_type::Long); + TS_ASSERT_EQUALS(tag_float().get_type() , tag_type::Float); + TS_ASSERT_EQUALS(tag_double().get_type() , tag_type::Double); + TS_ASSERT_EQUALS(tag_byte_array().get_type(), tag_type::Byte_Array); + TS_ASSERT_EQUALS(tag_string().get_type() , tag_type::String); + TS_ASSERT_EQUALS(tag_list().get_type() , tag_type::List); + TS_ASSERT_EQUALS(tag_compound().get_type() , tag_type::Compound); + TS_ASSERT_EQUALS(tag_int_array().get_type() , tag_type::Int_Array); + TS_ASSERT_EQUALS(tag_long_array().get_type(), tag_type::Long_Array); + } + + void test_tag_primitive() + { + tag_int tag(6); + TS_ASSERT_EQUALS(tag.get(), 6); + int& ref = tag; + ref = 12; + TS_ASSERT(tag == 12); + TS_ASSERT(tag != 6); + tag.set(24); + TS_ASSERT_EQUALS(ref, 24); + tag = 7; + TS_ASSERT_EQUALS(static_cast(tag), 7); + + TS_ASSERT_EQUALS(tag, tag_int(7)); + TS_ASSERT_DIFFERS(tag_float(2.5), tag_float(-2.5)); + TS_ASSERT_DIFFERS(tag_float(2.5), tag_double(2.5)); + + TS_ASSERT(tag_double() == 0.0); + + TS_ASSERT_EQUALS(tag_byte(INT8_MAX).get(), INT8_MAX); + TS_ASSERT_EQUALS(tag_byte(INT8_MIN).get(), INT8_MIN); + TS_ASSERT_EQUALS(tag_short(INT16_MAX).get(), INT16_MAX); + TS_ASSERT_EQUALS(tag_short(INT16_MIN).get(), INT16_MIN); + TS_ASSERT_EQUALS(tag_int(INT32_MAX).get(), INT32_MAX); + TS_ASSERT_EQUALS(tag_int(INT32_MIN).get(), INT32_MIN); + TS_ASSERT_EQUALS(tag_long(INT64_MAX).get(), INT64_MAX); + TS_ASSERT_EQUALS(tag_long(INT64_MIN).get(), INT64_MIN); + } + + void test_tag_string() + { + tag_string tag("foo"); + TS_ASSERT_EQUALS(tag.get(), "foo"); + std::string& ref = tag; + ref = "bar"; + TS_ASSERT_EQUALS(tag.get(), "bar"); + TS_ASSERT_DIFFERS(tag.get(), "foo"); + tag.set("baz"); + TS_ASSERT_EQUALS(ref, "baz"); + tag = "quux"; + TS_ASSERT_EQUALS("quux", static_cast(tag)); + std::string str("foo"); + tag = str; + TS_ASSERT_EQUALS(tag.get(),str); + + TS_ASSERT_EQUALS(tag_string(str).get(), "foo"); + TS_ASSERT_EQUALS(tag_string().get(), ""); + } + + void test_tag_compound() + { + tag_compound comp{ + {"foo", int16_t(12)}, + {"bar", "baz"}, + {"baz", -2.0}, + {"list", tag_list{16, 17}} + }; + + //Test assignments and conversions, and exceptions on bad conversions + TS_ASSERT_EQUALS(comp["foo"].get_type(), tag_type::Short); + TS_ASSERT_EQUALS(static_cast(comp["foo"]), 12); + TS_ASSERT_EQUALS(static_cast(comp.at("foo")), int16_t(12)); + TS_ASSERT(comp["foo"] == tag_short(12)); + TS_ASSERT_THROWS(static_cast(comp["foo"]), std::bad_cast); + TS_ASSERT_THROWS(static_cast(comp["foo"]), std::bad_cast); + + TS_ASSERT_THROWS(comp["foo"] = 32, std::bad_cast); + comp["foo"] = int8_t(32); + TS_ASSERT_EQUALS(static_cast(comp["foo"]), 32); + + TS_ASSERT_EQUALS(comp["bar"].get_type(), tag_type::String); + TS_ASSERT_EQUALS(static_cast(comp["bar"]), "baz"); + TS_ASSERT_THROWS(static_cast(comp["bar"]), std::bad_cast); + + TS_ASSERT_THROWS(comp["bar"] = -128, std::bad_cast); + comp["bar"] = "barbaz"; + TS_ASSERT_EQUALS(static_cast(comp["bar"]), "barbaz"); + + TS_ASSERT_EQUALS(comp["baz"].get_type(), tag_type::Double); + TS_ASSERT_EQUALS(static_cast(comp["baz"]), -2.0); + TS_ASSERT_THROWS(static_cast(comp["baz"]), std::bad_cast); + + //Test nested access + comp["quux"] = tag_compound{{"Hello", "World"}, {"zero", 0}}; + TS_ASSERT_EQUALS(comp.at("quux").get_type(), tag_type::Compound); + TS_ASSERT_EQUALS(static_cast(comp["quux"].at("Hello")), "World"); + TS_ASSERT_EQUALS(static_cast(comp["quux"]["Hello"]), "World"); + TS_ASSERT(comp["list"][1] == tag_int(17)); + + TS_ASSERT_THROWS(comp.at("nothing"), std::out_of_range); + + //Test equality comparisons + tag_compound comp2{ + {"foo", int16_t(32)}, + {"bar", "barbaz"}, + {"baz", -2.0}, + {"quux", tag_compound{{"Hello", "World"}, {"zero", 0}}}, + {"list", tag_list{16, 17}} + }; + TS_ASSERT(comp == comp2); + TS_ASSERT(comp != dynamic_cast(comp2["quux"].get())); + TS_ASSERT(comp != comp2["quux"]); + TS_ASSERT(dynamic_cast(comp["quux"].get()) == comp2["quux"]); + + //Test whether begin() through end() goes through all the keys and their + //values. The order of iteration is irrelevant there. + std::set keys{"bar", "baz", "foo", "list", "quux"}; + TS_ASSERT_EQUALS(comp2.size(), keys.size()); + unsigned int i = 0; + for(const std::pair& val: comp2) + { + TS_ASSERT_LESS_THAN(i, comp2.size()); + TS_ASSERT(keys.count(val.first)); + TS_ASSERT(val.second == comp2[val.first]); + ++i; + } + TS_ASSERT_EQUALS(i, comp2.size()); + + //Test erasing and has_key + TS_ASSERT_EQUALS(comp.erase("nothing"), false); + TS_ASSERT(comp.has_key("quux")); + TS_ASSERT(comp.has_key("quux", tag_type::Compound)); + TS_ASSERT(!comp.has_key("quux", tag_type::List)); + TS_ASSERT(!comp.has_key("quux", tag_type::Null)); + + TS_ASSERT_EQUALS(comp.erase("quux"), true); + TS_ASSERT(!comp.has_key("quux")); + TS_ASSERT(!comp.has_key("quux", tag_type::Compound)); + TS_ASSERT(!comp.has_key("quux", tag_type::Null)); + + comp.clear(); + TS_ASSERT(comp == tag_compound{}); + + //Test inserting values + TS_ASSERT_EQUALS(comp.put("abc", tag_double(6.0)).second, true); + TS_ASSERT_EQUALS(comp.put("abc", tag_long(-28)).second, false); + TS_ASSERT_EQUALS(comp.insert("ghi", tag_string("world")).second, true); + TS_ASSERT_EQUALS(comp.insert("abc", tag_string("hello")).second, false); + TS_ASSERT_EQUALS(comp.emplace("def", "ghi").second, true); + TS_ASSERT_EQUALS(comp.emplace("def", 4).second, false); + TS_ASSERT((comp == tag_compound{ + {"abc", tag_long(-28)}, + {"def", tag_byte(4)}, + {"ghi", tag_string("world")} + })); + } + + void test_value() + { + value val1; + value val2(make_unique(42)); + value val3(tag_int(42)); + + TS_ASSERT(!val1 && val2 && val3); + TS_ASSERT(val1 == val1); + TS_ASSERT(val1 != val2); + TS_ASSERT(val2 == val3); + TS_ASSERT(val3 == val3); + + value valstr(tag_string("foo")); + TS_ASSERT_EQUALS(static_cast(valstr), "foo"); + valstr = "bar"; + TS_ASSERT_THROWS(valstr = 5, std::bad_cast); + TS_ASSERT_EQUALS(static_cast(valstr), "bar"); + TS_ASSERT(valstr.as() == "bar"); + TS_ASSERT_EQUALS(&valstr.as(), &valstr.get()); + TS_ASSERT_THROWS(valstr.as(), std::bad_cast); + + val1 = int64_t(42); + TS_ASSERT(val2 != val1); + + TS_ASSERT_THROWS(val2 = int64_t(12), std::bad_cast); + TS_ASSERT_EQUALS(static_cast(val2), 42); + tag_int* ptr = dynamic_cast(val2.get_ptr().get()); + TS_ASSERT(*ptr == 42); + val2 = 52; + TS_ASSERT_EQUALS(static_cast(val2), 52); + TS_ASSERT(*ptr == 52); + + TS_ASSERT_THROWS(val1["foo"], std::bad_cast); + TS_ASSERT_THROWS(val1.at("foo"), std::bad_cast); + + val3 = 52; + TS_ASSERT(val2 == val3); + TS_ASSERT(val2.get_ptr() != val3.get_ptr()); + + val3 = std::move(val2); + TS_ASSERT(val3 == tag_int(52)); + TS_ASSERT(!val2); + + tag_int& tag = dynamic_cast(val3.get()); + TS_ASSERT(tag == tag_int(52)); + tag = 21; + TS_ASSERT_EQUALS(static_cast(val3), 21); + val1.set_ptr(std::move(val3.get_ptr())); + TS_ASSERT(val1.as() == 21); + + TS_ASSERT_EQUALS(val1.get_type(), tag_type::Int); + TS_ASSERT_EQUALS(val2.get_type(), tag_type::Null); + TS_ASSERT_EQUALS(val3.get_type(), tag_type::Null); + + val2 = val1; + val1 = val3; + TS_ASSERT(!val1 && val2 && !val3); + TS_ASSERT(val1.get_ptr() == nullptr); + TS_ASSERT(val2.get() == tag_int(21)); + TS_ASSERT(value(val1) == val1); + TS_ASSERT(value(val2) == val2); + val1 = val1; + val2 = val2; + TS_ASSERT(!val1); + TS_ASSERT(val1 == value_initializer(nullptr)); + TS_ASSERT(val2 == tag_int(21)); + + val3 = tag_short(2); + TS_ASSERT_THROWS(val3 = tag_string("foo"), std::bad_cast); + TS_ASSERT(val3.get() == tag_short(2)); + + val2.set_ptr(make_unique("foo")); + TS_ASSERT(val2 == tag_string("foo")); + } + + void test_tag_list() + { + tag_list list; + TS_ASSERT_EQUALS(list.el_type(), tag_type::Null); + TS_ASSERT_THROWS(list.push_back(value(nullptr)), std::invalid_argument); + + list.emplace_back("foo"); + TS_ASSERT_EQUALS(list.el_type(), tag_type::String); + list.push_back(tag_string("bar")); + TS_ASSERT_THROWS(list.push_back(tag_int(42)), std::invalid_argument); + TS_ASSERT_THROWS(list.emplace_back(), std::invalid_argument); + + TS_ASSERT((list == tag_list{"foo", "bar"})); + TS_ASSERT(list[0] == tag_string("foo")); + TS_ASSERT_EQUALS(static_cast(list.at(1)), "bar"); + + TS_ASSERT_EQUALS(list.size(), 2u); + TS_ASSERT_THROWS(list.at(2), std::out_of_range); + TS_ASSERT_THROWS(list.at(-1), std::out_of_range); + + list.set(1, value(tag_string("baz"))); + TS_ASSERT_THROWS(list.set(1, value(nullptr)), std::invalid_argument); + TS_ASSERT_THROWS(list.set(1, value(tag_int(-42))), std::invalid_argument); + TS_ASSERT_EQUALS(static_cast(list[1]), "baz"); + + TS_ASSERT_EQUALS(list.size(), 2u); + tag_string values[] = {"foo", "baz"}; + TS_ASSERT_EQUALS(list.end() - list.begin(), int(list.size())); + TS_ASSERT(std::equal(list.begin(), list.end(), values)); + + list.pop_back(); + TS_ASSERT(list == tag_list{"foo"}); + TS_ASSERT(list == tag_list::of({"foo"})); + TS_ASSERT(tag_list::of({"foo"}) == tag_list{"foo"}); + TS_ASSERT((list != tag_list{2, 3, 5, 7})); + + list.clear(); + TS_ASSERT_EQUALS(list.size(), 0u); + TS_ASSERT_EQUALS(list.el_type(), tag_type::String) + TS_ASSERT_THROWS(list.push_back(tag_short(25)), std::invalid_argument); + TS_ASSERT_THROWS(list.push_back(value(nullptr)), std::invalid_argument); + + list.reset(); + TS_ASSERT_EQUALS(list.el_type(), tag_type::Null); + list.emplace_back(17); + TS_ASSERT_EQUALS(list.el_type(), tag_type::Int); + + list.reset(tag_type::Float); + TS_ASSERT_EQUALS(list.el_type(), tag_type::Float); + list.emplace_back(17.0f); + TS_ASSERT(list == tag_list({17.0f})); + + TS_ASSERT(tag_list() != tag_list(tag_type::Int)); + TS_ASSERT(tag_list() == tag_list()); + TS_ASSERT(tag_list(tag_type::Short) != tag_list(tag_type::Int)); + TS_ASSERT(tag_list(tag_type::Short) == tag_list(tag_type::Short)); + + tag_list short_list = tag_list::of({25, 36}); + TS_ASSERT_EQUALS(short_list.el_type(), tag_type::Short); + TS_ASSERT((short_list == tag_list{int16_t(25), int16_t(36)})); + TS_ASSERT((short_list != tag_list{25, 36})); + TS_ASSERT((short_list == tag_list{value(tag_short(25)), value(tag_short(36))})); + + TS_ASSERT_THROWS((tag_list{value(tag_byte(4)), value(tag_int(5))}), std::invalid_argument); + TS_ASSERT_THROWS((tag_list{value(nullptr), value(tag_int(6))}), std::invalid_argument); + TS_ASSERT_THROWS((tag_list{value(tag_int(7)), value(tag_int(8)), value(nullptr)}), std::invalid_argument); + TS_ASSERT_EQUALS((tag_list(std::initializer_list{})).el_type(), tag_type::Null); + TS_ASSERT_EQUALS((tag_list{2, 3, 5, 7}).el_type(), tag_type::Int); + } + + void test_tag_byte_array() + { + std::vector vec{1, 2, 127, -128}; + tag_byte_array arr{1, 2, 127, -128}; + TS_ASSERT_EQUALS(arr.size(), 4u); + TS_ASSERT(arr.at(0) == 1 && arr[1] == 2 && arr[2] == 127 && arr.at(3) == -128); + TS_ASSERT_THROWS(arr.at(-1), std::out_of_range); + TS_ASSERT_THROWS(arr.at(4), std::out_of_range); + + TS_ASSERT(arr.get() == vec); + TS_ASSERT(arr == tag_byte_array(std::vector(vec))); + + arr.push_back(42); + vec.push_back(42); + + TS_ASSERT_EQUALS(arr.size(), 5u); + TS_ASSERT_EQUALS(arr.end() - arr.begin(), int(arr.size())); + TS_ASSERT(std::equal(arr.begin(), arr.end(), vec.begin())); + + arr.pop_back(); + arr.pop_back(); + TS_ASSERT_EQUALS(arr.size(), 3u); + TS_ASSERT((arr == tag_byte_array{1, 2, 127})); + TS_ASSERT((arr != tag_int_array{1, 2, 127})); + TS_ASSERT((arr != tag_long_array{1, 2, 127})); + TS_ASSERT((arr != tag_byte_array{1, 2, -1})); + + arr.clear(); + TS_ASSERT(arr == tag_byte_array()); + } + + void test_tag_int_array() + { + std::vector vec{100, 200, INT32_MAX, INT32_MIN}; + tag_int_array arr{100, 200, INT32_MAX, INT32_MIN}; + TS_ASSERT_EQUALS(arr.size(), 4u); + TS_ASSERT(arr.at(0) == 100 && arr[1] == 200 && arr[2] == INT32_MAX && arr.at(3) == INT32_MIN); + TS_ASSERT_THROWS(arr.at(-1), std::out_of_range); + TS_ASSERT_THROWS(arr.at(4), std::out_of_range); + + TS_ASSERT(arr.get() == vec); + TS_ASSERT(arr == tag_int_array(std::vector(vec))); + + arr.push_back(42); + vec.push_back(42); + + TS_ASSERT_EQUALS(arr.size(), 5u); + TS_ASSERT_EQUALS(arr.end() - arr.begin(), int(arr.size())); + TS_ASSERT(std::equal(arr.begin(), arr.end(), vec.begin())); + + arr.pop_back(); + arr.pop_back(); + TS_ASSERT_EQUALS(arr.size(), 3u); + TS_ASSERT((arr == tag_int_array{100, 200, INT32_MAX})); + TS_ASSERT((arr != tag_int_array{100, -56, -1})); + + arr.clear(); + TS_ASSERT(arr == tag_int_array()); + } + + void test_tag_long_array() + { + std::vector vec{100, 200, INT64_MAX, INT64_MIN}; + tag_long_array arr{100, 200, INT64_MAX, INT64_MIN}; + TS_ASSERT_EQUALS(arr.size(), 4u); + TS_ASSERT(arr.at(0) == 100 && arr[1] == 200 && arr[2] == INT64_MAX && arr.at(3) == INT64_MIN); + TS_ASSERT_THROWS(arr.at(-1), std::out_of_range); + TS_ASSERT_THROWS(arr.at(4), std::out_of_range); + + TS_ASSERT(arr.get() == vec); + TS_ASSERT(arr == tag_long_array(std::vector(vec))); + + arr.push_back(42); + vec.push_back(42); + + TS_ASSERT_EQUALS(arr.size(), 5u); + TS_ASSERT_EQUALS(arr.end() - arr.begin(), int(arr.size())); + TS_ASSERT(std::equal(arr.begin(), arr.end(), vec.begin())); + + arr.pop_back(); + arr.pop_back(); + TS_ASSERT_EQUALS(arr.size(), 3u); + TS_ASSERT((arr == tag_long_array{100, 200, INT64_MAX})); + TS_ASSERT((arr != tag_long_array{100, -56, -1})); + + arr.clear(); + TS_ASSERT(arr == tag_long_array()); + } + + void test_visitor() + { + struct : public nbt_visitor + { + tag* visited = nullptr; + + void visit(tag_byte& tag) { visited = &tag; } + void visit(tag_short& tag) { visited = &tag; } + void visit(tag_int& tag) { visited = &tag; } + void visit(tag_long& tag) { visited = &tag; } + void visit(tag_float& tag) { visited = &tag; } + void visit(tag_double& tag) { visited = &tag; } + void visit(tag_byte_array& tag) { visited = &tag; } + void visit(tag_string& tag) { visited = &tag; } + void visit(tag_list& tag) { visited = &tag; } + void visit(tag_compound& tag) { visited = &tag; } + void visit(tag_int_array& tag) { visited = &tag; } + void visit(tag_long_array& tag) { visited = &tag; } + } v; + + tag_byte b; b.accept(v); TS_ASSERT_EQUALS(v.visited, &b); + tag_short s; s.accept(v); TS_ASSERT_EQUALS(v.visited, &s); + tag_int i; i.accept(v); TS_ASSERT_EQUALS(v.visited, &i); + tag_long l; l.accept(v); TS_ASSERT_EQUALS(v.visited, &l); + tag_float f; f.accept(v); TS_ASSERT_EQUALS(v.visited, &f); + tag_double d; d.accept(v); TS_ASSERT_EQUALS(v.visited, &d); + tag_byte_array ba; ba.accept(v); TS_ASSERT_EQUALS(v.visited, &ba); + tag_string st; st.accept(v); TS_ASSERT_EQUALS(v.visited, &st); + tag_list ls; ls.accept(v); TS_ASSERT_EQUALS(v.visited, &ls); + tag_compound c; c.accept(v); TS_ASSERT_EQUALS(v.visited, &c); + tag_int_array ia; ia.accept(v); TS_ASSERT_EQUALS(v.visited, &ia); + tag_long_array la; la.accept(v); TS_ASSERT_EQUALS(v.visited, &la); + } +}; diff --git a/ultimmc/libraries/libnbtplusplus/test/read_test.h b/ultimmc/libraries/libnbtplusplus/test/read_test.h new file mode 100644 index 0000000..75ddbd5 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/test/read_test.h @@ -0,0 +1,250 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include +#include "io/stream_reader.h" +#ifdef NBT_HAVE_ZLIB +#include "io/izlibstream.h" +#endif +#include "nbt_tags.h" +#include +#include +#include + +using namespace nbt; + +#include "data.h" + +class read_test : public CxxTest::TestSuite +{ +public: + void test_stream_reader_big() + { + std::string input{ + 1, //tag_type::Byte + 0, //tag_type::End + 11, //tag_type::Int_Array + + 0x0a, 0x0b, 0x0c, 0x0d, //0x0a0b0c0d in Big Endian + + 0x00, 0x06, //String length in Big Endian + 'f', 'o', 'o', 'b', 'a', 'r', + + 0 //tag_type::End (invalid with allow_end = false) + }; + std::istringstream is(input); + nbt::io::stream_reader reader(is); + + TS_ASSERT_EQUALS(&reader.get_istr(), &is); + TS_ASSERT_EQUALS(reader.get_endian(), endian::big); + + TS_ASSERT_EQUALS(reader.read_type(), tag_type::Byte); + TS_ASSERT_EQUALS(reader.read_type(true), tag_type::End); + TS_ASSERT_EQUALS(reader.read_type(false), tag_type::Int_Array); + + int32_t i; + reader.read_num(i); + TS_ASSERT_EQUALS(i, 0x0a0b0c0d); + + TS_ASSERT_EQUALS(reader.read_string(), "foobar"); + + TS_ASSERT_THROWS(reader.read_type(false), io::input_error); + TS_ASSERT(!is); + is.clear(); + + //Test for invalid tag type 13 + is.str("\x0d"); + TS_ASSERT_THROWS(reader.read_type(), io::input_error); + TS_ASSERT(!is); + is.clear(); + + //Test for unexpcted EOF on numbers (input too short for int32_t) + is.str("\x03\x04"); + reader.read_num(i); + TS_ASSERT(!is); + } + + void test_stream_reader_little() + { + std::string input{ + 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, //0x0d0c0b0a09080706 in Little Endian + + 0x06, 0x00, //String length in Little Endian + 'f', 'o', 'o', 'b', 'a', 'r', + + 0x10, 0x00, //String length (intentionally too large) + 'a', 'b', 'c', 'd' //unexpected EOF + }; + std::istringstream is(input); + nbt::io::stream_reader reader(is, endian::little); + + TS_ASSERT_EQUALS(reader.get_endian(), endian::little); + + int64_t i; + reader.read_num(i); + TS_ASSERT_EQUALS(i, 0x0d0c0b0a09080706); + + TS_ASSERT_EQUALS(reader.read_string(), "foobar"); + + TS_ASSERT_THROWS(reader.read_string(), io::input_error); + TS_ASSERT(!is); + } + + //Tests if comp equals an extended variant of Notch's bigtest NBT + void verify_bigtest_structure(const tag_compound& comp) + { + TS_ASSERT_EQUALS(comp.size(), 13u); + + TS_ASSERT(comp.at("byteTest") == tag_byte(127)); + TS_ASSERT(comp.at("shortTest") == tag_short(32767)); + TS_ASSERT(comp.at("intTest") == tag_int(2147483647)); + TS_ASSERT(comp.at("longTest") == tag_long(9223372036854775807)); + TS_ASSERT(comp.at("floatTest") == tag_float(std::stof("0xff1832p-25"))); //0.4982315 + TS_ASSERT(comp.at("doubleTest") == tag_double(std::stod("0x1f8f6bbbff6a5ep-54"))); //0.493128713218231 + + //From bigtest.nbt: "the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...)" + tag_byte_array byteArrayTest; + for(int n = 0; n < 1000; ++n) + byteArrayTest.push_back((n*n*255 + n*7) % 100); + TS_ASSERT(comp.at("byteArrayTest (the first 1000 values of (n*n*255+n*7)%100, starting with n=0 (0, 62, 34, 16, 8, ...))") == byteArrayTest); + + TS_ASSERT(comp.at("stringTest") == tag_string("HELLO WORLD THIS IS A TEST STRING \u00C5\u00C4\u00D6!")); + + TS_ASSERT(comp.at("listTest (compound)") == tag_list::of({ + {{"created-on", tag_long(1264099775885)}, {"name", "Compound tag #0"}}, + {{"created-on", tag_long(1264099775885)}, {"name", "Compound tag #1"}} + })); + TS_ASSERT(comp.at("listTest (long)") == tag_list::of({11, 12, 13, 14, 15})); + TS_ASSERT(comp.at("listTest (end)") == tag_list()); + + TS_ASSERT((comp.at("nested compound test") == tag_compound{ + {"egg", tag_compound{{"value", 0.5f}, {"name", "Eggbert"}}}, + {"ham", tag_compound{{"value", 0.75f}, {"name", "Hampus"}}} + })); + + TS_ASSERT(comp.at("intArrayTest") == tag_int_array( + {0x00010203, 0x04050607, 0x08090a0b, 0x0c0d0e0f})); + } + + void test_read_bigtest() + { + //Uses an extended variant of Notch's original bigtest file + std::string input(__binary_bigtest_uncompr_start, __binary_bigtest_uncompr_end); + std::istringstream file(input, std::ios::binary); + + auto pair = nbt::io::read_compound(file); + TS_ASSERT_EQUALS(pair.first, "Level"); + verify_bigtest_structure(*pair.second); + } + + void test_read_littletest() + { + //Same as bigtest, but little endian + std::string input(__binary_littletest_uncompr_start, __binary_littletest_uncompr_end); + std::istringstream file(input, std::ios::binary); + + auto pair = nbt::io::read_compound(file, endian::little); + TS_ASSERT_EQUALS(pair.first, "Level"); + TS_ASSERT_EQUALS(pair.second->get_type(), tag_type::Compound); + verify_bigtest_structure(*pair.second); + } + + void test_read_eof1() + { + std::string input(__binary_errortest_eof1_start, __binary_errortest_eof1_end); + std::istringstream file(input, std::ios::binary); + nbt::io::stream_reader reader(file); + + //EOF within a tag_double payload + TS_ASSERT(file); + TS_ASSERT_THROWS(reader.read_tag(), io::input_error); + TS_ASSERT(!file); + } + + void test_read_eof2() + { + std::string input(__binary_errortest_eof2_start, __binary_errortest_eof2_end); + std::istringstream file(input, std::ios::binary); + nbt::io::stream_reader reader(file); + + //EOF within a key in a compound + TS_ASSERT(file); + TS_ASSERT_THROWS(reader.read_tag(), io::input_error); + TS_ASSERT(!file); + } + + void test_read_errortest_noend() + { + std::string input(__binary_errortest_noend_start, __binary_errortest_noend_end); + std::istringstream file(input, std::ios::binary); + nbt::io::stream_reader reader(file); + + //Missing tag_end + TS_ASSERT(file); + TS_ASSERT_THROWS(reader.read_tag(), io::input_error); + TS_ASSERT(!file); + } + + void test_read_errortest_neg_length() + { + std::string input(__binary_errortest_neg_length_start, __binary_errortest_neg_length_end); + std::istringstream file(input, std::ios::binary); + nbt::io::stream_reader reader(file); + + //Negative list length + TS_ASSERT(file); + TS_ASSERT_THROWS(reader.read_tag(), io::input_error); + TS_ASSERT(!file); + } + + void test_read_misc() + { + std::string input(__binary_toplevel_string_start, __binary_toplevel_string_end); + std::istringstream file(input, std::ios::binary); + nbt::io::stream_reader reader(file); + + //Toplevel tag other than compound + TS_ASSERT(file); + TS_ASSERT_THROWS(reader.read_compound(), io::input_error); + TS_ASSERT(!file); + + //Rewind and try again with read_tag + file.clear(); + TS_ASSERT(file.seekg(0)); + auto pair = reader.read_tag(); + TS_ASSERT_EQUALS(pair.first, "Test (toplevel tag_string)"); + TS_ASSERT(*pair.second == tag_string( + "Even though unprovided for by NBT, the library should also handle " + "the case where the file consists of something else than tag_compound")); + } + void test_read_gzip() + { +#ifdef NBT_HAVE_ZLIB + std::string input(__binary_bigtest_nbt_start, __binary_bigtest_nbt_end); + std::istringstream file(input, std::ios::binary); + zlib::izlibstream igzs(file); + TS_ASSERT(file && igzs); + + auto pair = nbt::io::read_compound(igzs); + TS_ASSERT(igzs); + TS_ASSERT_EQUALS(pair.first, "Level"); + verify_bigtest_structure(*pair.second); +#endif + } +}; diff --git a/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest.nbt b/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest.nbt new file mode 100644 index 0000000..de1a912 Binary files /dev/null and b/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest.nbt differ diff --git a/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest.zlib b/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest.zlib new file mode 100644 index 0000000..36aeee5 Binary files /dev/null and b/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest.zlib differ diff --git a/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest_corrupt.nbt b/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest_corrupt.nbt new file mode 100644 index 0000000..71eba42 Binary files /dev/null and b/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest_corrupt.nbt differ diff --git a/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest_eof.nbt b/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest_eof.nbt new file mode 100644 index 0000000..eeedb9d Binary files /dev/null and b/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest_eof.nbt differ diff --git a/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest_uncompr b/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest_uncompr new file mode 100644 index 0000000..dc1c9c1 Binary files /dev/null and b/ultimmc/libraries/libnbtplusplus/test/testfiles/bigtest_uncompr differ diff --git a/ultimmc/libraries/libnbtplusplus/test/testfiles/errortest_eof1 b/ultimmc/libraries/libnbtplusplus/test/testfiles/errortest_eof1 new file mode 100644 index 0000000..abb7ac5 Binary files /dev/null and b/ultimmc/libraries/libnbtplusplus/test/testfiles/errortest_eof1 differ diff --git a/ultimmc/libraries/libnbtplusplus/test/testfiles/errortest_eof2 b/ultimmc/libraries/libnbtplusplus/test/testfiles/errortest_eof2 new file mode 100644 index 0000000..1e9a503 Binary files /dev/null and b/ultimmc/libraries/libnbtplusplus/test/testfiles/errortest_eof2 differ diff --git a/ultimmc/libraries/libnbtplusplus/test/testfiles/errortest_neg_length b/ultimmc/libraries/libnbtplusplus/test/testfiles/errortest_neg_length new file mode 100644 index 0000000..228de89 Binary files /dev/null and b/ultimmc/libraries/libnbtplusplus/test/testfiles/errortest_neg_length differ diff --git a/ultimmc/libraries/libnbtplusplus/test/testfiles/errortest_noend b/ultimmc/libraries/libnbtplusplus/test/testfiles/errortest_noend new file mode 100644 index 0000000..d906146 Binary files /dev/null and b/ultimmc/libraries/libnbtplusplus/test/testfiles/errortest_noend differ diff --git a/ultimmc/libraries/libnbtplusplus/test/testfiles/littletest_uncompr b/ultimmc/libraries/libnbtplusplus/test/testfiles/littletest_uncompr new file mode 100644 index 0000000..86619e9 Binary files /dev/null and b/ultimmc/libraries/libnbtplusplus/test/testfiles/littletest_uncompr differ diff --git a/ultimmc/libraries/libnbtplusplus/test/testfiles/toplevel_string b/ultimmc/libraries/libnbtplusplus/test/testfiles/toplevel_string new file mode 100644 index 0000000..996cc78 Binary files /dev/null and b/ultimmc/libraries/libnbtplusplus/test/testfiles/toplevel_string differ diff --git a/ultimmc/libraries/libnbtplusplus/test/testfiles/trailing_data.zlib b/ultimmc/libraries/libnbtplusplus/test/testfiles/trailing_data.zlib new file mode 100644 index 0000000..83848f3 Binary files /dev/null and b/ultimmc/libraries/libnbtplusplus/test/testfiles/trailing_data.zlib differ diff --git a/ultimmc/libraries/libnbtplusplus/test/write_test.h b/ultimmc/libraries/libnbtplusplus/test/write_test.h new file mode 100644 index 0000000..8b16b2a --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/test/write_test.h @@ -0,0 +1,272 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include +#include "io/stream_writer.h" +#include "io/stream_reader.h" +#ifdef NBT_HAVE_ZLIB +#include "io/ozlibstream.h" +#include "io/izlibstream.h" +#endif +#include "nbt_tags.h" +#include +#include +#include + +#include "data.h" + +using namespace nbt; + +class read_test : public CxxTest::TestSuite +{ +public: + void test_stream_writer_big() + { + std::ostringstream os; + nbt::io::stream_writer writer(os); + + TS_ASSERT_EQUALS(&writer.get_ostr(), &os); + TS_ASSERT_EQUALS(writer.get_endian(), endian::big); + + writer.write_type(tag_type::End); + writer.write_type(tag_type::Long); + writer.write_type(tag_type::Int_Array); + + writer.write_num(int64_t(0x0102030405060708)); + + writer.write_string("foobar"); + + TS_ASSERT(os); + std::string expected{ + 0, //tag_type::End + 4, //tag_type::Long + 11, //tag_type::Int_Array + + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, //0x0102030405060708 in Big Endian + + 0x00, 0x06, //string length in Big Endian + 'f', 'o', 'o', 'b', 'a', 'r' + }; + TS_ASSERT_EQUALS(os.str(), expected); + + //too long for NBT + TS_ASSERT_THROWS(writer.write_string(std::string(65536, '.')), std::length_error); + TS_ASSERT(!os); + } + + void test_stream_writer_little() + { + std::ostringstream os; + nbt::io::stream_writer writer(os, endian::little); + + TS_ASSERT_EQUALS(writer.get_endian(), endian::little); + + writer.write_num(int32_t(0x0a0b0c0d)); + + writer.write_string("foobar"); + + TS_ASSERT(os); + std::string expected{ + 0x0d, 0x0c, 0x0b, 0x0a, //0x0a0b0c0d in Little Endian + + 0x06, 0x00, //string length in Little Endian + 'f', 'o', 'o', 'b', 'a', 'r' + }; + TS_ASSERT_EQUALS(os.str(), expected); + + TS_ASSERT_THROWS(writer.write_string(std::string(65536, '.')), std::length_error); + TS_ASSERT(!os); + } + + void test_write_payload_big() + { + std::ostringstream os; + nbt::io::stream_writer writer(os); + + //tag_primitive + writer.write_payload(tag_byte(127)); + writer.write_payload(tag_short(32767)); + writer.write_payload(tag_int(2147483647)); + writer.write_payload(tag_long(9223372036854775807)); + + //Same values as in endian_str_test + writer.write_payload(tag_float(std::stof("-0xCDEF01p-63"))); + writer.write_payload(tag_double(std::stod("-0x1DEF0102030405p-375"))); + + TS_ASSERT_EQUALS(os.str(), (std::string{ + '\x7F', + '\x7F', '\xFF', + '\x7F', '\xFF', '\xFF', '\xFF', + '\x7F', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', '\xFF', + + '\xAB', '\xCD', '\xEF', '\x01', + '\xAB', '\xCD', '\xEF', '\x01', '\x02', '\x03', '\x04', '\x05' + })); + os.str(""); //clear and reuse the stream + + //tag_string + writer.write_payload(tag_string("barbaz")); + TS_ASSERT_EQUALS(os.str(), (std::string{ + 0x00, 0x06, //string length in Big Endian + 'b', 'a', 'r', 'b', 'a', 'z' + })); + TS_ASSERT_THROWS(writer.write_payload(tag_string(std::string(65536, '.'))), std::length_error); + TS_ASSERT(!os); + os.clear(); + + //tag_byte_array + os.str(""); + writer.write_payload(tag_byte_array{0, 1, 127, -128, -127}); + TS_ASSERT_EQUALS(os.str(), (std::string{ + 0x00, 0x00, 0x00, 0x05, //length in Big Endian + 0, 1, 127, -128, -127 + })); + os.str(""); + + //tag_int_array + writer.write_payload(tag_int_array{0x01020304, 0x05060708, 0x090a0b0c}); + TS_ASSERT_EQUALS(os.str(), (std::string{ + 0x00, 0x00, 0x00, 0x03, //length in Big Endian + 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c + })); + os.str(""); + + //tag_list + writer.write_payload(tag_list()); //empty list with undetermined type, should be written as list of tag_end + writer.write_payload(tag_list(tag_type::Int)); //empty list of tag_int + writer.write_payload(tag_list{ //nested list + tag_list::of({0x3456, 0x789a}), + tag_list::of({0x0a, 0x0b, 0x0c, 0x0d}) + }); + TS_ASSERT_EQUALS(os.str(), (std::string{ + 0, //tag_type::End + 0x00, 0x00, 0x00, 0x00, //length + + 3, //tag_type::Int + 0x00, 0x00, 0x00, 0x00, //length + + 9, //tag_type::List + 0x00, 0x00, 0x00, 0x02, //length + //list 0 + 2, //tag_type::Short + 0x00, 0x00, 0x00, 0x02, //length + '\x34', '\x56', + '\x78', '\x9a', + //list 1 + 1, //tag_type::Byte + 0x00, 0x00, 0x00, 0x04, //length + 0x0a, + 0x0b, + 0x0c, + 0x0d + })); + os.str(""); + + //tag_compound + /* Testing if writing compounds works properly is problematic because the + order of the tags is not guaranteed. However with only two tags in a + compound we only have two possible orderings. + See below for a more thorough test that uses writing and re-reading. */ + writer.write_payload(tag_compound{}); + writer.write_payload(tag_compound{ + {"foo", "quux"}, + {"bar", tag_int(0x789abcde)} + }); + + std::string endtag{0x00}; + std::string subtag1{ + 8, //tag_type::String + 0x00, 0x03, //key length + 'f', 'o', 'o', + 0x00, 0x04, //string length + 'q', 'u', 'u', 'x' + }; + std::string subtag2{ + 3, //tag_type::Int + 0x00, 0x03, //key length + 'b', 'a', 'r', + '\x78', '\x9A', '\xBC', '\xDE' + }; + + TS_ASSERT(os.str() == endtag + subtag1 + subtag2 + endtag + || os.str() == endtag + subtag2 + subtag1 + endtag); + + //Now for write_tag: + os.str(""); + writer.write_tag("foo", tag_string("quux")); + TS_ASSERT_EQUALS(os.str(), subtag1); + TS_ASSERT(os); + + //too long key for NBT + TS_ASSERT_THROWS(writer.write_tag(std::string(65536, '.'), tag_long(-1)), std::length_error); + TS_ASSERT(!os); + } + + void test_write_bigtest() + { + /* Like already stated above, because no order is guaranteed for + tag_compound, we cannot simply test it by writing into a stream and directly + comparing the output to a reference value. + Instead, we assume that reading already works correctly and re-read the + written tag. + Smaller-grained tests are already done above. */ + std::string input(__binary_bigtest_uncompr_start, __binary_bigtest_uncompr_end); + std::istringstream file(input, std::ios::binary); + + const auto orig_pair = io::read_compound(file); + std::stringstream sstr; + + //Write into stream in Big Endian + io::write_tag(orig_pair.first, *orig_pair.second, sstr); + TS_ASSERT(sstr); + + //Read from stream in Big Endian and compare + auto written_pair = io::read_compound(sstr); + TS_ASSERT_EQUALS(orig_pair.first, written_pair.first); + TS_ASSERT(*orig_pair.second == *written_pair.second); + + sstr.str(""); //Reset and reuse stream + //Write into stream in Little Endian + io::write_tag(orig_pair.first, *orig_pair.second, sstr, endian::little); + TS_ASSERT(sstr); + + //Read from stream in Little Endian and compare + written_pair = io::read_compound(sstr, endian::little); + TS_ASSERT_EQUALS(orig_pair.first, written_pair.first); + TS_ASSERT(*orig_pair.second == *written_pair.second); + +#ifdef NBT_HAVE_ZLIB + //Now with gzip compression + sstr.str(""); + zlib::ozlibstream ogzs(sstr, -1, true); + io::write_tag(orig_pair.first, *orig_pair.second, ogzs); + ogzs.close(); + TS_ASSERT(ogzs); + TS_ASSERT(sstr); + //Read and compare + zlib::izlibstream igzs(sstr); + written_pair = io::read_compound(igzs); + TS_ASSERT(igzs); + TS_ASSERT_EQUALS(orig_pair.first, written_pair.first); + TS_ASSERT(*orig_pair.second == *written_pair.second); +#endif + } +}; diff --git a/ultimmc/libraries/libnbtplusplus/test/zlibstream_test.h b/ultimmc/libraries/libnbtplusplus/test/zlibstream_test.h new file mode 100644 index 0000000..26f86e0 --- /dev/null +++ b/ultimmc/libraries/libnbtplusplus/test/zlibstream_test.h @@ -0,0 +1,277 @@ +/* + * libnbt++ - A library for the Minecraft Named Binary Tag format. + * Copyright (C) 2013, 2015 ljfa-ag + * + * This file is part of libnbt++. + * + * libnbt++ is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * libnbt++ is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with libnbt++. If not, see . + */ +#include +#include "io/izlibstream.h" +#include "io/ozlibstream.h" +#include +#include + +#include "data.h" + +using namespace zlib; + +class zlibstream_test : public CxxTest::TestSuite +{ +private: + std::string bigtest; + +public: + zlibstream_test() + { + std::string input(__binary_bigtest_uncompr_start, __binary_bigtest_uncompr_end); + std::istringstream bigtest_f(input, std::ios::binary); + std::stringbuf bigtest_b; + bigtest_f >> &bigtest_b; + bigtest = bigtest_b.str(); + if(!bigtest_f || bigtest.size() == 0) + throw std::runtime_error("Could not read bigtest_uncompr file"); + } + + void test_inflate_gzip() + { + std::string input(__binary_bigtest_nbt_start, __binary_bigtest_nbt_end); + std::istringstream gzip_in(input, std::ios::binary); + TS_ASSERT(gzip_in); + + std::stringbuf data; + //Small buffer so not all fits at once (the compressed file is 561 bytes) + { + izlibstream igzs(gzip_in, 256); + igzs.exceptions(std::ios::failbit | std::ios::badbit); + TS_ASSERT(igzs.good()); + + TS_ASSERT_THROWS_NOTHING(igzs >> &data); + TS_ASSERT(igzs); + TS_ASSERT(igzs.eof()); + TS_ASSERT_EQUALS(data.str(), bigtest); + } + + //Clear and reuse buffers + data.str(""); + gzip_in.clear(); + gzip_in.seekg(0); + //Now try the same with larger buffer (but not large enough for all output, uncompressed size 1561 bytes) + { + izlibstream igzs(gzip_in, 1000); + igzs.exceptions(std::ios::failbit | std::ios::badbit); + TS_ASSERT(igzs.good()); + + TS_ASSERT_THROWS_NOTHING(igzs >> &data); + TS_ASSERT(igzs); + TS_ASSERT(igzs.eof()); + TS_ASSERT_EQUALS(data.str(), bigtest); + } + + data.str(""); + gzip_in.clear(); + gzip_in.seekg(0); + //Now with large buffer + { + izlibstream igzs(gzip_in, 4000); + igzs.exceptions(std::ios::failbit | std::ios::badbit); + TS_ASSERT(igzs.good()); + + TS_ASSERT_THROWS_NOTHING(igzs >> &data); + TS_ASSERT(igzs); + TS_ASSERT(igzs.eof()); + TS_ASSERT_EQUALS(data.str(), bigtest); + } + } + + void test_inflate_zlib() + { + std::string input(__binary_bigtest_zlib_start, __binary_bigtest_zlib_end); + std::istringstream zlib_in(input, std::ios::binary); + TS_ASSERT(zlib_in); + + std::stringbuf data; + izlibstream izls(zlib_in, 256); + izls.exceptions(std::ios::failbit | std::ios::badbit); + TS_ASSERT(izls.good()); + + TS_ASSERT_THROWS_NOTHING(izls >> &data); + TS_ASSERT(izls); + TS_ASSERT(izls.eof()); + TS_ASSERT_EQUALS(data.str(), bigtest); + } + + void test_inflate_corrupt() + { + std::string input(__binary_bigtest_corrupt_nbt_start, __binary_bigtest_corrupt_nbt_end); + std::istringstream gzip_in(input, std::ios::binary); + TS_ASSERT(gzip_in); + + std::vector buf(bigtest.size()); + izlibstream igzs(gzip_in); + igzs.exceptions(std::ios::failbit | std::ios::badbit); + TS_ASSERT_THROWS(igzs.read(buf.data(), buf.size()), zlib_error); + TS_ASSERT(igzs.bad()); + } + + void test_inflate_eof() + { + std::string input(__binary_bigtest_eof_nbt_start, __binary_bigtest_eof_nbt_end); + std::istringstream gzip_in(input, std::ios::binary); + TS_ASSERT(gzip_in); + + std::vector buf(bigtest.size()); + izlibstream igzs(gzip_in); + igzs.exceptions(std::ios::failbit | std::ios::badbit); + TS_ASSERT_THROWS(igzs.read(buf.data(), buf.size()), zlib_error); + TS_ASSERT(igzs.bad()); + } + + void test_inflate_trailing() + { + //This file contains additional uncompressed data after the zlib-compressed data + std::string input(__binary_trailing_data_zlib_start, __binary_trailing_data_zlib_end); + std::istringstream file(input, std::ios::binary); + izlibstream izls(file, 32); + TS_ASSERT(file && izls); + + std::string str; + izls >> str; + TS_ASSERT(izls); + TS_ASSERT(izls.eof()); + TS_ASSERT_EQUALS(str, "foobar"); + + //Now read the uncompressed data + TS_ASSERT(file); + TS_ASSERT(!file.eof()); + file >> str; + TS_ASSERT(!file.bad()); + TS_ASSERT_EQUALS(str, "barbaz"); + } + + void test_deflate_zlib() + { + //Here we assume that inflating works and has already been tested + std::stringstream str; + std::stringbuf output; + //Small buffer + { + ozlibstream ozls(str, -1, false, 256); + ozls.exceptions(std::ios::failbit | std::ios::badbit); + TS_ASSERT_THROWS_NOTHING(ozls << bigtest); + TS_ASSERT(ozls.good()); + TS_ASSERT_THROWS_NOTHING(ozls.close()); + TS_ASSERT(ozls.good()); + } + TS_ASSERT(str.good()); + { + izlibstream izls(str); + TS_ASSERT_THROWS_NOTHING(izls >> &output); + TS_ASSERT(izls); + } + TS_ASSERT_EQUALS(output.str(), bigtest); + + str.clear(); str.str(""); + output.str(""); + //Medium sized buffer + //Write first half, then flush and write second half + { + ozlibstream ozls(str, 9, false, 512); + ozls.exceptions(std::ios::failbit | std::ios::badbit); + + std::string half1 = bigtest.substr(0, bigtest.size()/2); + std::string half2 = bigtest.substr(bigtest.size()/2); + TS_ASSERT_THROWS_NOTHING(ozls << half1 << std::flush << half2); + TS_ASSERT(ozls.good()); + TS_ASSERT_THROWS_NOTHING(ozls.close()); + TS_ASSERT(ozls.good()); + } + TS_ASSERT(str.good()); + { + izlibstream izls(str); + izls >> &output; + TS_ASSERT(izls); + } + TS_ASSERT_EQUALS(output.str(), bigtest); + + str.clear(); str.str(""); + output.str(""); + //Large buffer + { + ozlibstream ozls(str, 1, false, 4000); + ozls.exceptions(std::ios::failbit | std::ios::badbit); + TS_ASSERT_THROWS_NOTHING(ozls << bigtest); + TS_ASSERT(ozls.good()); + TS_ASSERT_THROWS_NOTHING(ozls.close()); + TS_ASSERT_THROWS_NOTHING(ozls.close()); //closing twice shouldn't be a problem + TS_ASSERT(ozls.good()); + } + TS_ASSERT(str.good()); + { + izlibstream izls(str); + izls >> &output; + TS_ASSERT(izls); + } + TS_ASSERT_EQUALS(output.str(), bigtest); + } + + void test_deflate_gzip() + { + std::stringstream str; + std::stringbuf output; + { + ozlibstream ozls(str, -1, true); + ozls.exceptions(std::ios::failbit | std::ios::badbit); + TS_ASSERT_THROWS_NOTHING(ozls << bigtest); + TS_ASSERT(ozls.good()); + TS_ASSERT_THROWS_NOTHING(ozls.close()); + TS_ASSERT(ozls.good()); + } + TS_ASSERT(str.good()); + { + izlibstream izls(str); + izls >> &output; + TS_ASSERT(izls); + } + TS_ASSERT_EQUALS(output.str(), bigtest); + } + + void test_deflate_closed() + { + std::stringstream str; + { + ozlibstream ozls(str); + ozls.exceptions(std::ios::failbit | std::ios::badbit); + TS_ASSERT_THROWS_NOTHING(ozls << bigtest); + TS_ASSERT_THROWS_NOTHING(ozls.close()); + TS_ASSERT_THROWS_NOTHING(ozls << "foo"); + TS_ASSERT_THROWS_ANYTHING(ozls.close()); + TS_ASSERT(ozls.bad()); + TS_ASSERT(!str); + } + str.clear(); + str.seekp(0); + { + ozlibstream ozls(str); + //this time without exceptions + TS_ASSERT_THROWS_NOTHING(ozls << bigtest); + TS_ASSERT_THROWS_NOTHING(ozls.close()); + TS_ASSERT_THROWS_NOTHING(ozls << "foo" << std::flush); + TS_ASSERT(ozls.bad()); + TS_ASSERT_THROWS_NOTHING(ozls.close()); + TS_ASSERT(ozls.bad()); + TS_ASSERT(!str); + } + } +}; diff --git a/ultimmc/libraries/optional-bare/CMakeLists.txt b/ultimmc/libraries/optional-bare/CMakeLists.txt new file mode 100644 index 0000000..b8b498c --- /dev/null +++ b/ultimmc/libraries/optional-bare/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.1) +project(optional-bare) + +add_library(optional-bare INTERFACE) +target_include_directories(optional-bare INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include") diff --git a/ultimmc/libraries/optional-bare/LICENSE.txt b/ultimmc/libraries/optional-bare/LICENSE.txt new file mode 100644 index 0000000..36b7cd9 --- /dev/null +++ b/ultimmc/libraries/optional-bare/LICENSE.txt @@ -0,0 +1,23 @@ +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +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, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/ultimmc/libraries/optional-bare/README.md b/ultimmc/libraries/optional-bare/README.md new file mode 100644 index 0000000..e29ff7c --- /dev/null +++ b/ultimmc/libraries/optional-bare/README.md @@ -0,0 +1,5 @@ +# optional bare + +A simple single-file header-only version of a C++17-like optional for default-constructible, copyable types, for C++98 and later. + +Imported from: https://github.com/martinmoene/optional-bare/commit/0bb1d183bcee1e854c4ea196b533252c51f98b81 diff --git a/ultimmc/libraries/optional-bare/include/nonstd/optional b/ultimmc/libraries/optional-bare/include/nonstd/optional new file mode 100644 index 0000000..ecbfa03 --- /dev/null +++ b/ultimmc/libraries/optional-bare/include/nonstd/optional @@ -0,0 +1,508 @@ +// +// Copyright 2017-2019 by Martin Moene +// +// https://github.com/martinmoene/optional-bare +// +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef NONSTD_OPTIONAL_BARE_HPP +#define NONSTD_OPTIONAL_BARE_HPP + +#define optional_bare_MAJOR 1 +#define optional_bare_MINOR 1 +#define optional_bare_PATCH 0 + +#define optional_bare_VERSION optional_STRINGIFY(optional_bare_MAJOR) "." optional_STRINGIFY(optional_bare_MINOR) "." optional_STRINGIFY(optional_bare_PATCH) + +#define optional_STRINGIFY( x ) optional_STRINGIFY_( x ) +#define optional_STRINGIFY_( x ) #x + +// optional-bare configuration: + +#define optional_OPTIONAL_DEFAULT 0 +#define optional_OPTIONAL_NONSTD 1 +#define optional_OPTIONAL_STD 2 + +#if !defined( optional_CONFIG_SELECT_OPTIONAL ) +# define optional_CONFIG_SELECT_OPTIONAL ( optional_HAVE_STD_OPTIONAL ? optional_OPTIONAL_STD : optional_OPTIONAL_NONSTD ) +#endif + +// Control presence of exception handling (try and auto discover): + +#ifndef optional_CONFIG_NO_EXCEPTIONS +# if _MSC_VER +# include // for _HAS_EXCEPTIONS +# endif +# if _MSC_VER +# include // for _HAS_EXCEPTIONS +# endif +# if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || (_HAS_EXCEPTIONS) +# define optional_CONFIG_NO_EXCEPTIONS 0 +# else +# define optional_CONFIG_NO_EXCEPTIONS 1 +# endif +#endif + +// C++ language version detection (C++20 is speculative): +// Note: VC14.0/1900 (VS2015) lacks too much from C++14. + +#ifndef optional_CPLUSPLUS +# if defined(_MSVC_LANG ) && !defined(__clang__) +# define optional_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG ) +# else +# define optional_CPLUSPLUS __cplusplus +# endif +#endif + +#define optional_CPP98_OR_GREATER ( optional_CPLUSPLUS >= 199711L ) +#define optional_CPP11_OR_GREATER ( optional_CPLUSPLUS >= 201103L ) +#define optional_CPP14_OR_GREATER ( optional_CPLUSPLUS >= 201402L ) +#define optional_CPP17_OR_GREATER ( optional_CPLUSPLUS >= 201703L ) +#define optional_CPP20_OR_GREATER ( optional_CPLUSPLUS >= 202000L ) + +// C++ language version (represent 98 as 3): + +#define optional_CPLUSPLUS_V ( optional_CPLUSPLUS / 100 - (optional_CPLUSPLUS > 200000 ? 2000 : 1994) ) + +// Use C++17 std::optional if available and requested: + +#if optional_CPP17_OR_GREATER && defined(__has_include ) +# if __has_include( ) +# define optional_HAVE_STD_OPTIONAL 1 +# else +# define optional_HAVE_STD_OPTIONAL 0 +# endif +#else +# define optional_HAVE_STD_OPTIONAL 0 +#endif + +#define optional_USES_STD_OPTIONAL ( (optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_STD) || ((optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_DEFAULT) && optional_HAVE_STD_OPTIONAL) ) + +// +// Using std::optional: +// + +#if optional_USES_STD_OPTIONAL + +#include +#include + +namespace nonstd { + + using std::in_place; + using std::in_place_type; + using std::in_place_index; + using std::in_place_t; + using std::in_place_type_t; + using std::in_place_index_t; + + using std::optional; + using std::bad_optional_access; + using std::hash; + + using std::nullopt; + using std::nullopt_t; + + using std::operator==; + using std::operator!=; + using std::operator<; + using std::operator<=; + using std::operator>; + using std::operator>=; + using std::make_optional; + using std::swap; +} + +#else // optional_USES_STD_OPTIONAL + +#include + +#if ! optional_CONFIG_NO_EXCEPTIONS +# include +#endif + +namespace nonstd { namespace optional_bare { + +// type for nullopt + +struct nullopt_t +{ + struct init{}; + nullopt_t( init ) {} +}; + +// extra parenthesis to prevent the most vexing parse: + +const nullopt_t nullopt(( nullopt_t::init() )); + +// optional access error. + +#if ! optional_CONFIG_NO_EXCEPTIONS + +class bad_optional_access : public std::logic_error +{ +public: + explicit bad_optional_access() + : logic_error( "bad optional access" ) {} +}; + +#endif // optional_CONFIG_NO_EXCEPTIONS + +// Simplistic optional: requires T to be default constructible, copyable. + +template< typename T > +class optional +{ +private: + typedef void (optional::*safe_bool)() const; + +public: + typedef T value_type; + + optional() + : has_value_( false ) + {} + + optional( nullopt_t ) + : has_value_( false ) + {} + + optional( T const & arg ) + : has_value_( true ) + , value_ ( arg ) + {} + + template< class U > + optional( optional const & other ) + : has_value_( other.has_value() ) + , value_ ( other.value() ) + {} + + optional & operator=( nullopt_t ) + { + reset(); + return *this; + } + + template< class U > + optional & operator=( optional const & other ) + { + has_value_ = other.has_value(); + value_ = other.value(); + return *this; + } + + void swap( optional & rhs ) + { + using std::swap; + if ( has_value() == true && rhs.has_value() == true ) { swap( **this, *rhs ); } + else if ( has_value() == false && rhs.has_value() == true ) { initialize( *rhs ); rhs.reset(); } + else if ( has_value() == true && rhs.has_value() == false ) { rhs.initialize( **this ); reset(); } + } + + // observers + + value_type const * operator->() const + { + return assert( has_value() ), + &value_; + } + + value_type * operator->() + { + return assert( has_value() ), + &value_; + } + + value_type const & operator*() const + { + return assert( has_value() ), + value_; + } + + value_type & operator*() + { + return assert( has_value() ), + value_; + } + +#if optional_CPP11_OR_GREATER + explicit operator bool() const + { + return has_value(); + } +#else + operator safe_bool() const + { + return has_value() ? &optional::this_type_does_not_support_comparisons : 0; + } +#endif + + bool has_value() const + { + return has_value_; + } + + value_type const & value() const + { +#if optional_CONFIG_NO_EXCEPTIONS + assert( has_value() ); +#else + if ( ! has_value() ) + throw bad_optional_access(); +#endif + return value_; + } + + value_type & value() + { +#if optional_CONFIG_NO_EXCEPTIONS + assert( has_value() ); +#else + if ( ! has_value() ) + throw bad_optional_access(); +#endif + return value_; + } + + template< class U > + value_type value_or( U const & v ) const + { + return has_value() ? value() : static_cast( v ); + } + + // modifiers + + void reset() + { + has_value_ = false; + } + +private: + void this_type_does_not_support_comparisons() const {} + + template< typename V > + void initialize( V const & value ) + { + assert( ! has_value() ); + value_ = value; + has_value_ = true; + } + +private: + bool has_value_; + value_type value_; +}; + +// Relational operators + +template< typename T, typename U > +inline bool operator==( optional const & x, optional const & y ) +{ + return bool(x) != bool(y) ? false : bool(x) == false ? true : *x == *y; +} + +template< typename T, typename U > +inline bool operator!=( optional const & x, optional const & y ) +{ + return !(x == y); +} + +template< typename T, typename U > +inline bool operator<( optional const & x, optional const & y ) +{ + return (!y) ? false : (!x) ? true : *x < *y; +} + +template< typename T, typename U > +inline bool operator>( optional const & x, optional const & y ) +{ + return (y < x); +} + +template< typename T, typename U > +inline bool operator<=( optional const & x, optional const & y ) +{ + return !(y < x); +} + +template< typename T, typename U > +inline bool operator>=( optional const & x, optional const & y ) +{ + return !(x < y); +} + +// Comparison with nullopt + +template< typename T > +inline bool operator==( optional const & x, nullopt_t ) +{ + return (!x); +} + +template< typename T > +inline bool operator==( nullopt_t, optional const & x ) +{ + return (!x); +} + +template< typename T > +inline bool operator!=( optional const & x, nullopt_t ) +{ + return bool(x); +} + +template< typename T > +inline bool operator!=( nullopt_t, optional const & x ) +{ + return bool(x); +} + +template< typename T > +inline bool operator<( optional const &, nullopt_t ) +{ + return false; +} + +template< typename T > +inline bool operator<( nullopt_t, optional const & x ) +{ + return bool(x); +} + +template< typename T > +inline bool operator<=( optional const & x, nullopt_t ) +{ + return (!x); +} + +template< typename T > +inline bool operator<=( nullopt_t, optional const & ) +{ + return true; +} + +template< typename T > +inline bool operator>( optional const & x, nullopt_t ) +{ + return bool(x); +} + +template< typename T > +inline bool operator>( nullopt_t, optional const & ) +{ + return false; +} + +template< typename T > +inline bool operator>=( optional const &, nullopt_t ) +{ + return true; +} + +template< typename T > +inline bool operator>=( nullopt_t, optional const & x ) +{ + return (!x); +} + +// Comparison with T + +template< typename T, typename U > +inline bool operator==( optional const & x, U const & v ) +{ + return bool(x) ? *x == v : false; +} + +template< typename T, typename U > +inline bool operator==( U const & v, optional const & x ) +{ + return bool(x) ? v == *x : false; +} + +template< typename T, typename U > +inline bool operator!=( optional const & x, U const & v ) +{ + return bool(x) ? *x != v : true; +} + +template< typename T, typename U > +inline bool operator!=( U const & v, optional const & x ) +{ + return bool(x) ? v != *x : true; +} + +template< typename T, typename U > +inline bool operator<( optional const & x, U const & v ) +{ + return bool(x) ? *x < v : true; +} + +template< typename T, typename U > +inline bool operator<( U const & v, optional const & x ) +{ + return bool(x) ? v < *x : false; +} + +template< typename T, typename U > +inline bool operator<=( optional const & x, U const & v ) +{ + return bool(x) ? *x <= v : true; +} + +template< typename T, typename U > +inline bool operator<=( U const & v, optional const & x ) +{ + return bool(x) ? v <= *x : false; +} + +template< typename T, typename U > +inline bool operator>( optional const & x, U const & v ) +{ + return bool(x) ? *x > v : false; +} + +template< typename T, typename U > +inline bool operator>( U const & v, optional const & x ) +{ + return bool(x) ? v > *x : true; +} + +template< typename T, typename U > +inline bool operator>=( optional const & x, U const & v ) +{ + return bool(x) ? *x >= v : false; +} + +template< typename T, typename U > +inline bool operator>=( U const & v, optional const & x ) +{ + return bool(x) ? v >= *x : true; +} + +// Specialized algorithms + +template< typename T > +void swap( optional & x, optional & y ) +{ + x.swap( y ); +} + +// Convenience function to create an optional. + +template< typename T > +inline optional make_optional( T const & v ) +{ + return optional( v ); +} + +} // namespace optional-bare + +using namespace optional_bare; + +} // namespace nonstd + +#endif // optional_USES_STD_OPTIONAL + +#endif // NONSTD_OPTIONAL_BARE_HPP diff --git a/ultimmc/libraries/quazip/CMakeLists.txt b/ultimmc/libraries/quazip/CMakeLists.txt new file mode 100644 index 0000000..66c7d2d --- /dev/null +++ b/ultimmc/libraries/quazip/CMakeLists.txt @@ -0,0 +1,87 @@ +cmake_minimum_required(VERSION 3.1) + +project(Launcher_quazip) + +#### LIBRARY #### +find_package(ZLIB REQUIRED) +find_package(Qt5Core REQUIRED) + +set(QUAZIP_SRC + quazip/crypt.h + quazip/ioapi.h + quazip/JlCompress.cpp + quazip/JlCompress.h + quazip/qioapi.cpp + quazip/quaadler32.cpp + quazip/quaadler32.h + quazip/quachecksum32.h + quazip/quacrc32.cpp + quazip/quacrc32.h + quazip/quagzipfile.cpp + quazip/quagzipfile.h + quazip/quaziodevice.cpp + quazip/quaziodevice.h + quazip/quazip.cpp + quazip/quazip.h + quazip/quazip_global.h + quazip/quazipdir.cpp + quazip/quazipdir.h + quazip/quazipfile.cpp + quazip/quazipfile.h + quazip/quazipfileinfo.cpp + quazip/quazipfileinfo.h + quazip/quazipnewinfo.cpp + quazip/quazipnewinfo.h + quazip/unzip.c + quazip/unzip.h + quazip/zip.c + quazip/zip.h +) + +if (Qt5_POSITION_INDEPENDENT_CODE) + SET(CMAKE_POSITION_INDEPENDENT_CODE ON) +endif() + +add_library(Launcher_quazip SHARED ${QUAZIP_SRC}) +target_include_directories(Launcher_quazip PUBLIC "quazip" "${CMAKE_CURRENT_BINARY_DIR}" PRIVATE ${ZLIB_INCLUDE_DIRS}) +target_link_libraries(Launcher_quazip Qt5::Core ${ZLIB_LIBRARIES}) +target_compile_definitions(Launcher_quazip PRIVATE "-DQUAZIP_BUILD") +set_target_properties(Launcher_quazip PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN 1) + +# Install it +install( + TARGETS Launcher_quazip + RUNTIME DESTINATION ${LIBRARY_DEST_DIR} + LIBRARY DESTINATION ${LIBRARY_DEST_DIR} +) + +#### UNIT TESTS #### +find_package(Qt5Network REQUIRED) +find_package(Qt5Test REQUIRED) + +set(QUAZIP_TEST_SRC + qztest/qztest.cpp + qztest/qztest.h + qztest/testjlcompress.cpp + qztest/testjlcompress.h + qztest/testquachecksum32.cpp + qztest/testquachecksum32.h + qztest/testquagzipfile.cpp + qztest/testquagzipfile.h + qztest/testquaziodevice.cpp + qztest/testquaziodevice.h + qztest/testquazip.cpp + qztest/testquazip.h + qztest/testquazipdir.cpp + qztest/testquazipdir.h + qztest/testquazipfile.cpp + qztest/testquazipfile.h + qztest/testquazipfileinfo.cpp + qztest/testquazipfileinfo.h + qztest/testquazipnewinfo.cpp + qztest/testquazipnewinfo.h +) + +add_executable(Launcher_quazip_test ${QUAZIP_TEST_SRC}) +target_link_libraries(Launcher_quazip_test Launcher_quazip Qt5::Network Qt5::Test) +add_test(NAME quazip_testsuite COMMAND Launcher_quazip_test) diff --git a/ultimmc/libraries/quazip/COPYING b/ultimmc/libraries/quazip/COPYING new file mode 100644 index 0000000..e5e2603 --- /dev/null +++ b/ultimmc/libraries/quazip/COPYING @@ -0,0 +1,474 @@ +The QuaZIP library is licensed under the GNU Lesser General Public +License V2.1 plus a static linking exception. + +STATIC LINKING EXCEPTION + +The copyright holders give you permission to link this library with +independent modules to produce an executable, regardless of the license +terms of these independent modules, and to copy and distribute the +resulting executable under terms of your choice, provided that you also +meet, for each linked independent module, the terms and conditions of +the license of that module. An independent module is a module which is +not derived from or based on this library. If you modify this library, +you must extend this exception to your version of the library. + +The text of the GNU Lesser General Public License V2.1 follows. + + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/ultimmc/libraries/quazip/NEWS.txt b/ultimmc/libraries/quazip/NEWS.txt new file mode 100644 index 0000000..3d0be94 --- /dev/null +++ b/ultimmc/libraries/quazip/NEWS.txt @@ -0,0 +1,163 @@ +QuaZIP changes + +* 2017-02-05 0.7.3 + * Symlink handling + * Static linking exception for LGPL + * Minor bug fixes + +* 2016-03-29 0.7.2 + * New JlCompress methods (QIODevice*-based API by Lukasz Kwiecinski) + * Implemented QuaZioDevice::atEnd() and bytesAvailable()--these might + break ABI, but pretty unlikely. + +* 2015-01-07 0.7.1 + * Fixed licensing issues (bug #45). + * Added the convenience method QuaZipFileInfo::isEncrypted(). + +* 2014-07-24 0.7 + * It is now possible to write ZIP files to sequential devices + like sockets (only in mdCreate mode, so no self-extract, sorry). + * A few zip64 fixes. + * Several bug fixes and portability improvements. + +* 2014-02-09 0.6.2 + * QuaZipNewInfo / QuaZipFileInfo64 now provide API to access/set + NTFS time stamps - useful even on non-NTFS systems if you + need more precise dates and times than default ones. + * QuaZipNewInfo may now be initialized from QuaZipFileInfo64. + * No more crashes when using QSaveFile as QIODevice for ZIP. + * The new QuaZip::setAutoClose() method allows to leave the + QIODevice open when you close the QuaZip instance. + * qztest now depends on quazip, no longer breaking the build. + +* 2014-01-26 0.6.1 + * Improved zip64 support. + * A LOT more tests thanks to g++ --coverage / lcov. + * JlCompress extraction methods now create files with default + permissions if they are zero in the original archive. + * Some QuaZipDir fixes (thanks to the new tests). + +* 2014-01-22 0.6 + * Minizip updated to 1.1 (with all the necessary modifications + re-done), and that means that... + * the long-awaited zip64 support is now available! + * A few rather minor fixes. + +* 2014-01-19 0.5.2 + * Some minor bug fixes. + * API to access file permissions subfield of the external + attributes. + * MS VS 2012 Express support. + * API to set the default codec used to encode/decode file names + (mainly for use by various wrappers such as JlCompress, when + you don't have direct access to the underlying QuaZip instance). + +* 2013-03-02 0.5.1 + * Lots of QuaZipDir fixes, thanks to all bug reporters. + * Full Qt Creator support. + * MS VS 2010 Express support. + * Qt5 support (didn't need any source code changes anyway). + * Lots of minor bug fixes. + +* 2012-09-07 0.5 + * Added run_moc.bat files for building under Windows in case Qt + integration is not available (e. g. VS 2008 Express). + * Added the QuaZipDir class to simplify ZIP navigation in terms + of directories. + * Added the QuaGzipFile class for working with GZIP archives. It + was added as a bonus since it has nothing to do with the main + purpose of the library. It probably won't get any major + improvements, although minor bug fixes are possible. + * Added the QuaZIODevice class for working with zlib + compression. It has nothing to do with the ZIP format, and + therefore the same notice as for the QuaGzipFile applies. + * The global comment is no longer erased when adding files to + an archive. + * Many bug fixes. + +* 2012-01-14 0.4.4 + * Fixed isSequential() test that was causing open() failures on + Unix. + * Fixed sub-directory compressing in JlCompress. + * Added MS VS 2008 solution, compatible with the binary Qt + distribution (tested on MS VS 2008 Express, had to run MOC + manually due to the lack of plugin in Express). + * Fixed extracting directories in JlCompress. + * Fixed JlCompress.h includes in the test suite, which used + lowercase names thus breaking on case-sensitive systems. + * Implemented missing QuaZipFile::getZip() that was only + declared. + * Fixed reopening closed files. + * Fixed possible memory leak in case of open error. + +* 2011-09-09 0.4.3 + * New test suite using QTestLib. + * Fixed bytesAvailable(), pos() and atEnd(). + * Added ZIP v1.0 support and disabling data descriptor for + compatibility with some older software. + * Fixed DLL export/import issues for some symbols. + * Added QUAZIP_STATIC macro for compiling as a static library or + directly including the source. + * Added getFileNameList() and getFileInfoList() convenience + functions. + * Added some buffering to JlCompress to improve performance. + +* 2011-08-10 0.4.2 + * Cmake patch (thanks to Bernhard Rosenkraenzer). + * Symbian patch (thanks to Hamish Willee). + * Documented the multiple files limitation of QuaZipFile. + * Fixed relative paths handling in JlCompress. + * Fixed linking to MinGW zlib. + +* 2011-05-26 0.4.1 + * License statement updated to avoid confusion. GPL license + removed for the very same reason. + * Parts of original package are now clearly marked as modified, + just as their license requires. + +* 2011-05-23 0.4 + * QuaZip and QuaZipFile classes now use the Pimpl idiom. This + means that future releases will probably be binary compatible + with this one, but it also means that this one is binary + incompatible with the old ones. + * IO API has been rewritten using QIODevice instead of standard + C library. Among other things it means that QuaZip now supports + files up to 4 GB in size instead of 2 GB. + * Added QuaZip methods allowing access to ZIP files represented + by any seekable QIODevice implementation (QBuffer is a good + example). + +* 2010-07-23 0.3 + * Fixed getComment() for global comments. + * Added some useful classes for calculating checksums (thanks to + Adam Walczak). + * Added some utility classes for working with whole directories + (thanks to Roberto Pompermaier). It would be nice if someone + documents these in English, though. + * Probably fixed some problems with passwords (thanks to Vasiliy + Sorokin). I didn't test it, though. + +* 2008-09-17 0.2.3 + * Fixed license notices in sources. + +* SVN + * Fixed a small bug in QuaZipFile::atEnd(). + +* 2007-01-16 0.2.2 + * Added LGPL as alternative license. + * Added FAQ documentation page. + +* 2006-03-21 0.2.1 + * Fixed setCommentCodec() bug. + * Fixed bug that set month 1-12 instead of 0-11, as specified in + zip.h. + * Added workaround for Qt's bug that caused wrong timestamps. + * Few documentation fixes and cosmetic changes. + +* 2005-07-08 0.2 + * Write support. + * Extended QuaZipFile API, including size(), *pos() functions. + * Support for comments encoding/decoding. + +* 2005-07-01 0.1 + * Initial version. diff --git a/ultimmc/libraries/quazip/quazip/JlCompress.cpp b/ultimmc/libraries/quazip/quazip/JlCompress.cpp new file mode 100644 index 0000000..9bf7032 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/JlCompress.cpp @@ -0,0 +1,512 @@ +/* +Copyright (C) 2010 Roberto Pompermaier +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include "JlCompress.h" +#include + +bool JlCompress::copyData(QIODevice &inFile, QIODevice &outFile) +{ + while (!inFile.atEnd()) { + char buf[4096]; + qint64 readLen = inFile.read(buf, 4096); + if (readLen <= 0) + return false; + if (outFile.write(buf, readLen) != readLen) + return false; + } + return true; +} + +bool JlCompress::removeFile(QStringList listFile) +{ + bool ret = true; + // Per ogni file + for (int i=0; i& added, QString prefix, + const FilterFunction filter) +{ + if (!zip) return false; + if (zip->getMode()!=QuaZip::mdCreate && zip->getMode()!=QuaZip::mdAppend && zip->getMode()!=QuaZip::mdAdd) + { + return false; + } + + QDir directory(dir); + if (!directory.exists()) + { + return false; + } + + QDir origDirectory(origDir); + if (dir != origDir) + { + QString internalDirName = origDirectory.relativeFilePath(dir); + if(!filter || !filter(internalDirName)) + { + QuaZipFile dirZipFile(zip); + QString dirPrefix; + if(prefix.isEmpty()) + { + dirPrefix = origDirectory.relativeFilePath(dir) + "/"; + } + else + { + dirPrefix = prefix + '/' + origDirectory.relativeFilePath(dir) + "/"; + } + if (!dirZipFile.open(QIODevice::WriteOnly, QuaZipNewInfo(dirPrefix, dir), 0, 0, 0)) + { + return false; + } + dirZipFile.close(); + } + } + + QFileInfoList files = directory.entryInfoList(QDir::AllDirs | QDir::NoDotAndDotDot | QDir::Hidden); + for (auto file: files) + { + if(!file.isDir()) + { + continue; + } + if(!compressSubDir(zip,file.absoluteFilePath(),origDir, added, prefix, filter)) + { + return false; + } + } + + files = directory.entryInfoList(QDir::Files | QDir::Hidden); + for (auto file: files) + { + if(!file.isFile()) + { + continue; + } + + if(file.absoluteFilePath()==zip->getZipName()) + { + continue; + } + + QString filename = origDirectory.relativeFilePath(file.absoluteFilePath()); + if(filter && filter(filename)) + { + continue; + } + if(prefix.size()) + { + filename = prefix + '/' + filename; + } + added.insert(filename); + if (!JlCompress::compressFile(zip,file.absoluteFilePath(),filename)) + { + return false; + } + } + + return true; +} + +bool JlCompress::extractFile(QuaZip* zip, QString fileName, QString fileDest) { + // zip: oggetto dove aggiungere il file + // filename: nome del file reale + // fileincompress: nome del file all'interno del file compresso + + // Controllo l'apertura dello zip + if (!zip) return false; + if (zip->getMode()!=QuaZip::mdUnzip) return false; + + // Apro il file compresso + if (!fileName.isEmpty()) + zip->setCurrentFile(fileName); + QuaZipFile inFile(zip); + if(!inFile.open(QIODevice::ReadOnly) || inFile.getZipError()!=UNZ_OK) return false; + + // Controllo esistenza cartella file risultato + QDir curDir; + if (fileDest.endsWith('/')) { + if (!curDir.mkpath(fileDest)) { + return false; + } + } else { + if (!curDir.mkpath(QFileInfo(fileDest).absolutePath())) { + return false; + } + } + + QuaZipFileInfo64 info; + if (!zip->getCurrentFileInfo(&info)) + return false; + + // QFile::Permissions srcPerm = info.getPermissions(); + if (fileDest.endsWith('/') && QFileInfo(fileDest).isDir()) { + /* + if (srcPerm != 0) { + QFile(fileDest).setPermissions(srcPerm); + } + */ + return true; + } + + // Apro il file risultato + QFile outFile; + outFile.setFileName(fileDest); + if(!outFile.open(QIODevice::WriteOnly)) return false; + + // Copio i dati + if (!copyData(inFile, outFile) || inFile.getZipError()!=UNZ_OK) { + outFile.close(); + removeFile(QStringList(fileDest)); + return false; + } + outFile.close(); + + // Chiudo i file + inFile.close(); + if (inFile.getZipError()!=UNZ_OK) { + removeFile(QStringList(fileDest)); + return false; + } + + /* + if (srcPerm != 0) { + outFile.setPermissions(srcPerm); + } + */ + return true; +} + +QString JlCompress::extractFile(QuaZip &zip, QString fileName, QString fileDest) +{ + if(!zip.open(QuaZip::mdUnzip)) { + return QString(); + } + + // Estraggo il file + if (fileDest.isEmpty()) + fileDest = fileName; + if (!extractFile(&zip,fileName,fileDest)) { + return QString(); + } + + // Chiudo il file zip + zip.close(); + if(zip.getZipError()!=0) { + removeFile(QStringList(fileDest)); + return QString(); + } + return QFileInfo(fileDest).absoluteFilePath(); +} + +QString JlCompress::extractFile(QString fileCompressed, QString fileName, QString fileDest) +{ + // Apro lo zip + QuaZip zip(fileCompressed); + return extractFile(zip, fileName, fileDest); +} + +QString JlCompress::extractFile(QIODevice *ioDevice, QString fileName, QString fileDest) +{ + QuaZip zip(ioDevice); + return extractFile(zip, fileName, fileDest); +} + +bool JlCompress::compressFile(QuaZip* zip, QString fileName, QString fileDest) +{ + // zip: oggetto dove aggiungere il file + // fileName: nome del file reale + // fileDest: nome del file all'interno del file compresso + + // Controllo l'apertura dello zip + if (!zip) return false; + if (zip->getMode()!=QuaZip::mdCreate && + zip->getMode()!=QuaZip::mdAppend && + zip->getMode()!=QuaZip::mdAdd) return false; + + // Apro il file originale + QFile inFile; + inFile.setFileName(fileName); + if(!inFile.open(QIODevice::ReadOnly)) return false; + + // Apro il file risulato + QuaZipFile outFile(zip); + if(!outFile.open(QIODevice::WriteOnly, QuaZipNewInfo(fileDest, inFile.fileName()))) return false; + + // Copio i dati + if (!copyData(inFile, outFile) || outFile.getZipError()!=UNZ_OK) { + return false; + } + + // Chiudo i file + outFile.close(); + if (outFile.getZipError()!=UNZ_OK) return false; + inFile.close(); + + return true; +} + +bool JlCompress::compressFile(QString fileCompressed, QString file) +{ + // Creo lo zip + QuaZip zip(fileCompressed); + QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); + if(!zip.open(QuaZip::mdCreate)) { + QFile::remove(fileCompressed); + return false; + } + + // Aggiungo il file + if (!compressFile(&zip,file,QFileInfo(file).fileName())) { + QFile::remove(fileCompressed); + return false; + } + + // Chiudo il file zip + zip.close(); + if(zip.getZipError()!=0) { + QFile::remove(fileCompressed); + return false; + } + + return true; +} + +bool JlCompress::compressFiles(QString fileCompressed, QStringList files) +{ + // Creo lo zip + QuaZip zip(fileCompressed); + QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); + if(!zip.open(QuaZip::mdCreate)) { + QFile::remove(fileCompressed); + return false; + } + + // Comprimo i file + QFileInfo info; + Q_FOREACH (QString file, files) { + info.setFile(file); + if (!info.exists() || !compressFile(&zip,file,info.fileName())) { + QFile::remove(fileCompressed); + return false; + } + } + + // Chiudo il file zip + zip.close(); + if(zip.getZipError()!=0) { + QFile::remove(fileCompressed); + return false; + } + + return true; +} +/* +bool JlCompress::compressDir(QString fileCompressed, QString dir, bool recursive) +{ + return compressDir(fileCompressed, dir, recursive, 0); +} + +bool JlCompress::compressDir(QString fileCompressed, QString dir, bool recursive, QDir::Filters filters) +{ + // Creo lo zip + QuaZip zip(fileCompressed); + QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); + if(!zip.open(QuaZip::mdCreate)) { + QFile::remove(fileCompressed); + return false; + } + + // Aggiungo i file e le sotto cartelle + if (!compressSubDir(&zip,dir,dir,recursive, filters)) { + QFile::remove(fileCompressed); + return false; + } + + // Chiudo il file zip + zip.close(); + if(zip.getZipError()!=0) { + QFile::remove(fileCompressed); + return false; + } + + return true; +} +*/ +bool JlCompress::compressDir(QString fileCompressed, QString dir, QString prefix, const FilterFunction filter) +{ + QuaZip zip(fileCompressed); + QDir().mkpath(QFileInfo(fileCompressed).absolutePath()); + if(!zip.open(QuaZip::mdCreate)) + { + QFile::remove(fileCompressed); + return false; + } + + QSet added; + if (!compressSubDir(&zip, dir, dir, added, prefix, filter)) + { + QFile::remove(fileCompressed); + return false; + } + zip.close(); + if(zip.getZipError()!=0) + { + QFile::remove(fileCompressed); + return false; + } + return true; +} + +QStringList JlCompress::extractFiles(QString fileCompressed, QStringList files, QString dir) +{ + // Creo lo zip + QuaZip zip(fileCompressed); + return extractFiles(zip, files, dir); +} + +QStringList JlCompress::extractFiles(QuaZip &zip, const QStringList &files, const QString &dir) +{ + if(!zip.open(QuaZip::mdUnzip)) { + return QStringList(); + } + + // Estraggo i file + QStringList extracted; + for (int i=0; iopen(QuaZip::mdUnzip)) { + delete zip; + return QStringList(); + } + + // Estraggo i nomi dei file + QStringList lst; + QuaZipFileInfo64 info; + for(bool more=zip->goToFirstFile(); more; more=zip->goToNextFile()) { + if(!zip->getCurrentFileInfo(&info)) { + delete zip; + return QStringList(); + } + lst << info.name; + //info.name.toLocal8Bit().constData() + } + + // Chiudo il file zip + zip->close(); + if(zip->getZipError()!=0) { + delete zip; + return QStringList(); + } + delete zip; + return lst; +} + +QStringList JlCompress::extractDir(QIODevice *ioDevice, QString dir) +{ + QuaZip zip(ioDevice); + return extractDir(zip, dir); +} + +QStringList JlCompress::getFileList(QIODevice *ioDevice) +{ + QuaZip *zip = new QuaZip(ioDevice); + return getFileList(zip); +} + +QStringList JlCompress::extractFiles(QIODevice *ioDevice, QStringList files, QString dir) +{ + QuaZip zip(ioDevice); + return extractFiles(zip, files, dir); +} + diff --git a/ultimmc/libraries/quazip/quazip/JlCompress.h b/ultimmc/libraries/quazip/quazip/JlCompress.h new file mode 100644 index 0000000..b25be2c --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/JlCompress.h @@ -0,0 +1,182 @@ +#ifndef JLCOMPRESSFOLDER_H_ +#define JLCOMPRESSFOLDER_H_ + +/* +Copyright (C) 2010 Roberto Pompermaier +Copyright (C) 2005-2016 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include "quazip.h" +#include "quazipfile.h" +#include "quazipfileinfo.h" +#include +#include +#include +#include +#include + +/// Utility class for typical operations. +/** + This class contains a number of useful static functions to perform + simple operations, such as mass ZIP packing or extraction. + */ +class QUAZIP_EXPORT JlCompress { +public: + using FilterFunction = std::function; +public: + static bool copyData(QIODevice &inFile, QIODevice &outFile); + static bool removeFile(QStringList listFile); +public: + /// Compress a single file. + /** + \param fileCompressed The name of the archive. + \param file The file to compress. + \return true if success, false otherwise. + */ + static bool compressFile(QString fileCompressed, QString file); + static bool compressFile(QuaZip* zip, QString fileName, QString fileDest); + /// Compress a list of files. + /** + \param fileCompressed The name of the archive. + \param files The file list to compress. + \return true if success, false otherwise. + */ + static bool compressFiles(QString fileCompressed, QStringList files); + /// Compress a whole directory. + /** + Does not compress hidden files. See compressDir(QString, QString, bool, QDir::Filters). + + \param fileCompressed The name of the archive. + \param dir The directory to compress. + \param recursive Whether to pack the subdirectories as well, or + just regular files. + \return true if success, false otherwise. + */ + //static bool compressDir(QString fileCompressed, QString dir = QString(), bool recursive = true); + /** + * @brief Compress a whole directory. + * + * Unless filters are specified explicitly, packs + * only regular non-hidden files (and subdirs, if @c recursive is true). + * If filters are specified, they are OR-combined with + * %QDir::AllDirs|%QDir::NoDotAndDotDot when searching for dirs + * and with QDir::Files when searching for files. + * + * @param fileCompressed path to the resulting archive + * @param dir path to the directory being compressed + * @param recursive if true, then the subdirectories are packed as well + * @param filters what to pack, filters are applied both when searching + * for subdirs (if packing recursively) and when looking for files to pack + * @return true on success, false otherwise + */ + //static bool compressDir(QString fileCompressed, QString dir, bool recursive, QDir::Filters filters); + + // prefix is added to all the compressed files paths + static bool compressDir(QString fileCompressed, QString dir, QString prefix = QString(), const FilterFunction filter = nullptr); + /// Compress a subdirectory. + /** + \param parentZip Opened zip containing the parent directory. + \param dir The full path to the directory to pack. + \param parentDir The full path to the directory corresponding to + the root of the ZIP. + \param prefix Prefix added to all paths inside the archive + \param filter Filter function that decides whether a particular filename goes into the archive + \return true if success, false otherwise. + */ + static bool compressSubDir(QuaZip *parentZip, QString dir, QString parentDir, QSet &added, QString prefix = QString(), + const FilterFunction filter = nullptr); +public: + /// Extract a single file. + /** + \param fileCompressed The name of the archive. + \param fileName The file to extract. + \param fileDest The destination file, assumed to be identical to + \a file if left empty. + \return The list of the full paths of the files extracted, empty on failure. + */ + static QString extractFile(QString fileCompressed, QString fileName, QString fileDest = QString()); + static QString extractFile(QuaZip &zip, QString fileName, QString fileDest); + static bool extractFile(QuaZip* zip, QString fileName, QString fileDest); + + /// Extract a list of files. + /** + \param fileCompressed The name of the archive. + \param files The file list to extract. + \param dir The directory to put the files to, the current + directory if left empty. + \return The list of the full paths of the files extracted, empty on failure. + */ + static QStringList extractFiles(QString fileCompressed, QStringList files, QString dir = QString()); + static QStringList extractFiles(QuaZip &zip, const QStringList &files, const QString &dir); + + /// Extract a whole archive. + /** + \param fileCompressed The name of the archive. + \param dir The directory to extract to, the current directory if + left empty. + \return The list of the full paths of the files extracted, empty on failure. + */ + static QStringList extractDir(QString fileCompressed, QString dir = QString()); + static QStringList extractDir(QuaZip &zip, const QString &dir); + + /// Get the file list. + /** + \return The list of the files in the archive, or, more precisely, the + list of the entries, including both files and directories if they + are present separately. + */ + static QStringList getFileList(QString fileCompressed); + static QStringList getFileList(QIODevice *ioDevice); + static QStringList getFileList(QuaZip *zip); + + /// Extract a single file. + /** + \param ioDevice pointer to device with compressed data. + \param fileName The file to extract. + \param fileDest The destination file, assumed to be identical to + \a file if left empty. + \return The list of the full paths of the files extracted, empty on failure. + */ + static QString extractFile(QIODevice *ioDevice, QString fileName, QString fileDest = QString()); + + /// Extract a list of files. + /** + \param ioDevice pointer to device with compressed data. + \param files The file list to extract. + \param dir The directory to put the files to, the current + directory if left empty. + \return The list of the full paths of the files extracted, empty on failure. + */ + static QStringList extractFiles(QIODevice *ioDevice, QStringList files, QString dir = QString()); + + /// Extract a whole archive. + /** + \param ioDevice pointer to device with compressed data. + \param dir The directory to extract to, the current directory if + left empty. + \return The list of the full paths of the files extracted, empty on failure. + */ + static QStringList extractDir(QIODevice *ioDevice, QString dir = QString()); +}; + +#endif /* JLCOMPRESSFOLDER_H_ */ diff --git a/ultimmc/libraries/quazip/quazip/crypt.h b/ultimmc/libraries/quazip/quazip/crypt.h new file mode 100644 index 0000000..ddee28e --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/crypt.h @@ -0,0 +1,135 @@ +/* crypt.h -- base code for crypt/uncrypt ZIPfile + + + Version 1.01e, February 12th, 2005 + + Copyright (C) 1998-2005 Gilles Vollant + + This code is a modified version of crypting code in Infozip distribution + + The encryption/decryption parts of this source code (as opposed to the + non-echoing password parts) were originally written in Europe. The + whole source package can be freely distributed, including from the USA. + (Prior to January 2000, re-export from the US was a violation of US law.) + + This encryption code is a direct transcription of the algorithm from + Roger Schlafly, described by Phil Katz in the file appnote.txt. This + file (appnote.txt) is distributed with the PKZIP program (even in the + version without encryption capabilities). + + If you don't need crypting in your application, just define symbols + NOCRYPT and NOUNCRYPT. + + This code support the "Traditional PKWARE Encryption". + + The new AES encryption added on Zip format by Winzip (see the page + http://www.winzip.com/aes_info.htm ) and PKWare PKZip 5.x Strong + Encryption is not supported. +*/ + +#include "quazip_global.h" + +#define CRC32(c, b) ((*(pcrc_32_tab+(((int)(c) ^ (b)) & 0xff))) ^ ((c) >> 8)) + +/*********************************************************************** + * Return the next byte in the pseudo-random sequence + */ +static int decrypt_byte(unsigned long* pkeys, const z_crc_t FAR * pcrc_32_tab UNUSED) +{ + //(void) pcrc_32_tab; /* avoid "unused parameter" warning */ + unsigned temp; /* POTENTIAL BUG: temp*(temp^1) may overflow in an + * unpredictable manner on 16-bit systems; not a problem + * with any known compiler so far, though */ + + temp = ((unsigned)(*(pkeys+2)) & 0xffff) | 2; + return (int)(((temp * (temp ^ 1)) >> 8) & 0xff); +} + +/*********************************************************************** + * Update the encryption keys with the next byte of plain text + */ +static int update_keys(unsigned long* pkeys,const z_crc_t FAR * pcrc_32_tab,int c) +{ + (*(pkeys+0)) = CRC32((*(pkeys+0)), c); + (*(pkeys+1)) += (*(pkeys+0)) & 0xff; + (*(pkeys+1)) = (*(pkeys+1)) * 134775813L + 1; + { + register int keyshift = (int)((*(pkeys+1)) >> 24); + (*(pkeys+2)) = CRC32((*(pkeys+2)), keyshift); + } + return c; +} + + +/*********************************************************************** + * Initialize the encryption keys and the random header according to + * the given password. + */ +static void init_keys(const char* passwd,unsigned long* pkeys,const z_crc_t FAR * pcrc_32_tab) +{ + *(pkeys+0) = 305419896L; + *(pkeys+1) = 591751049L; + *(pkeys+2) = 878082192L; + while (*passwd != '\0') { + update_keys(pkeys,pcrc_32_tab,(int)*passwd); + passwd++; + } +} + +#define zdecode(pkeys,pcrc_32_tab,c) \ + (update_keys(pkeys,pcrc_32_tab,c ^= decrypt_byte(pkeys,pcrc_32_tab))) + +#define zencode(pkeys,pcrc_32_tab,c,t) \ + (t=decrypt_byte(pkeys,pcrc_32_tab), update_keys(pkeys,pcrc_32_tab,c), t^(c)) + +#ifdef INCLUDECRYPTINGCODE_IFCRYPTALLOWED + +#define RAND_HEAD_LEN 12 + /* "last resort" source for second part of crypt seed pattern */ +# ifndef ZCR_SEED2 +# define ZCR_SEED2 3141592654UL /* use PI as default pattern */ +# endif + +static int crypthead(passwd, buf, bufSize, pkeys, pcrc_32_tab, crcForCrypting) + const char *passwd; /* password string */ + unsigned char *buf; /* where to write header */ + int bufSize; + unsigned long* pkeys; + const z_crc_t FAR * pcrc_32_tab; + unsigned long crcForCrypting; +{ + int n; /* index in random header */ + int t; /* temporary */ + int c; /* random byte */ + unsigned char header[RAND_HEAD_LEN-2]; /* random header */ + static unsigned calls = 0; /* ensure different random header each time */ + + if (bufSize> 7) & 0xff; + header[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, c, t); + } + /* Encrypt random header (last two bytes is high word of crc) */ + init_keys(passwd, pkeys, pcrc_32_tab); + for (n = 0; n < RAND_HEAD_LEN-2; n++) + { + buf[n] = (unsigned char)zencode(pkeys, pcrc_32_tab, header[n], t); + } + buf[n++] = zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 16) & 0xff, t); + buf[n++] = zencode(pkeys, pcrc_32_tab, (int)(crcForCrypting >> 24) & 0xff, t); + return n; +} + +#endif diff --git a/ultimmc/libraries/quazip/quazip/ioapi.h b/ultimmc/libraries/quazip/quazip/ioapi.h new file mode 100644 index 0000000..bbb94c8 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/ioapi.h @@ -0,0 +1,207 @@ +/* ioapi.h -- IO base function header for compress/uncompress .zip + part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html ) + + Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html ) + + Modifications for Zip64 support + Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com ) + + Modified by Sergey A. Tachenov to allow QIODevice API usage. + + For more info read MiniZip_info.txt + + Changes + + Oct-2009 - Defined ZPOS64_T to fpos_t on windows and u_int64_t on linux. (might need to find a better why for this) + Oct-2009 - Change to fseeko64, ftello64 and fopen64 so large files would work on linux. + More if/def section may be needed to support other platforms + Oct-2009 - Defined fxxxx64 calls to normal fopen/ftell/fseek so they would compile on windows. + (but you should use iowin32.c for windows instead) + +*/ + +#ifndef _ZLIBIOAPI64_H +#define _ZLIBIOAPI64_H + +#if (!defined(_WIN32)) && (!defined(WIN32)) + + // Linux needs this to support file operation on files larger then 4+GB + // But might need better if/def to select just the platforms that needs them. + + #ifndef __USE_FILE_OFFSET64 + #define __USE_FILE_OFFSET64 + #endif + #ifndef __USE_LARGEFILE64 + #define __USE_LARGEFILE64 + #endif + #ifndef _LARGEFILE64_SOURCE + #define _LARGEFILE64_SOURCE + #endif + #ifndef _FILE_OFFSET_BIT + #define _FILE_OFFSET_BIT 64 + #endif +#endif + +#include +#include +#include "zlib.h" + +#if defined(USE_FILE32API) +#define fopen64 fopen +#define ftello64 ftell +#define fseeko64 fseek +#else +#ifdef _MSC_VER + #define fopen64 fopen + #if (_MSC_VER >= 1400) && (!(defined(NO_MSCVER_FILE64_FUNC))) + #define ftello64 _ftelli64 + #define fseeko64 _fseeki64 + #else // old MSC + #define ftello64 ftell + #define fseeko64 fseek + #endif +#endif +#endif + +/* +#ifndef ZPOS64_T + #ifdef _WIN32 + #define ZPOS64_T fpos_t + #else + #include + #define ZPOS64_T uint64_t + #endif +#endif +*/ + +#ifdef HAVE_MINIZIP64_CONF_H +#include "mz64conf.h" +#endif + +/* a type choosen by DEFINE */ +#ifdef HAVE_64BIT_INT_CUSTOM +typedef 64BIT_INT_CUSTOM_TYPE ZPOS64_T; +#else +#ifdef HAS_STDINT_H +#include "stdint.h" +typedef uint64_t ZPOS64_T; +#else + + +#if defined(_MSC_VER) || defined(__BORLANDC__) +typedef unsigned __int64 ZPOS64_T; +#else +typedef unsigned long long int ZPOS64_T; +#endif +#endif +#endif + + + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef OF +#define OF _Z_OF +#endif + +#define ZLIB_FILEFUNC_SEEK_CUR (1) +#define ZLIB_FILEFUNC_SEEK_END (2) +#define ZLIB_FILEFUNC_SEEK_SET (0) + +#define ZLIB_FILEFUNC_MODE_READ (1) +#define ZLIB_FILEFUNC_MODE_WRITE (2) +#define ZLIB_FILEFUNC_MODE_READWRITEFILTER (3) + +#define ZLIB_FILEFUNC_MODE_EXISTING (4) +#define ZLIB_FILEFUNC_MODE_CREATE (8) + + +#ifndef ZCALLBACK + #if (defined(WIN32) || defined(_WIN32) || defined (WINDOWS) || defined (_WINDOWS)) && defined(CALLBACK) && defined (USEWINDOWS_CALLBACK) + #define ZCALLBACK CALLBACK + #else + #define ZCALLBACK + #endif +#endif + + + + +typedef voidpf (ZCALLBACK *open_file_func) OF((voidpf opaque, voidpf file, int mode)); +typedef uLong (ZCALLBACK *read_file_func) OF((voidpf opaque, voidpf stream, void* buf, uLong size)); +typedef uLong (ZCALLBACK *write_file_func) OF((voidpf opaque, voidpf stream, const void* buf, uLong size)); +typedef int (ZCALLBACK *close_file_func) OF((voidpf opaque, voidpf stream)); +typedef int (ZCALLBACK *testerror_file_func) OF((voidpf opaque, voidpf stream)); + +typedef uLong (ZCALLBACK *tell_file_func) OF((voidpf opaque, voidpf stream)); +typedef int (ZCALLBACK *seek_file_func) OF((voidpf opaque, voidpf stream, uLong offset, int origin)); + + +/* here is the "old" 32 bits structure structure */ +typedef struct zlib_filefunc_def_s +{ + open_file_func zopen_file; + read_file_func zread_file; + write_file_func zwrite_file; + tell_file_func ztell_file; + seek_file_func zseek_file; + close_file_func zclose_file; + testerror_file_func zerror_file; + voidpf opaque; +} zlib_filefunc_def; + +typedef ZPOS64_T (ZCALLBACK *tell64_file_func) OF((voidpf opaque, voidpf stream)); +typedef int (ZCALLBACK *seek64_file_func) OF((voidpf opaque, voidpf stream, ZPOS64_T offset, int origin)); +typedef voidpf (ZCALLBACK *open64_file_func) OF((voidpf opaque, voidpf file, int mode)); + +typedef struct zlib_filefunc64_def_s +{ + open64_file_func zopen64_file; + read_file_func zread_file; + write_file_func zwrite_file; + tell64_file_func ztell64_file; + seek64_file_func zseek64_file; + close_file_func zclose_file; + testerror_file_func zerror_file; + voidpf opaque; + close_file_func zfakeclose_file; // for no-auto-close flag +} zlib_filefunc64_def; + +void fill_qiodevice64_filefunc OF((zlib_filefunc64_def* pzlib_filefunc_def)); +void fill_qiodevice_filefunc OF((zlib_filefunc_def* pzlib_filefunc_def)); + +/* now internal definition, only for zip.c and unzip.h */ +typedef struct zlib_filefunc64_32_def_s +{ + zlib_filefunc64_def zfile_func64; + open_file_func zopen32_file; + tell_file_func ztell32_file; + seek_file_func zseek32_file; +} zlib_filefunc64_32_def; + + +#define ZREAD64(filefunc,filestream,buf,size) ((*((filefunc).zfile_func64.zread_file)) ((filefunc).zfile_func64.opaque,filestream,buf,size)) +#define ZWRITE64(filefunc,filestream,buf,size) ((*((filefunc).zfile_func64.zwrite_file)) ((filefunc).zfile_func64.opaque,filestream,buf,size)) +//#define ZTELL64(filefunc,filestream) ((*((filefunc).ztell64_file)) ((filefunc).opaque,filestream)) +//#define ZSEEK64(filefunc,filestream,pos,mode) ((*((filefunc).zseek64_file)) ((filefunc).opaque,filestream,pos,mode)) +#define ZCLOSE64(filefunc,filestream) ((*((filefunc).zfile_func64.zclose_file)) ((filefunc).zfile_func64.opaque,filestream)) +#define ZFAKECLOSE64(filefunc,filestream) ((*((filefunc).zfile_func64.zfakeclose_file)) ((filefunc).zfile_func64.opaque,filestream)) +#define ZERROR64(filefunc,filestream) ((*((filefunc).zfile_func64.zerror_file)) ((filefunc).zfile_func64.opaque,filestream)) + +voidpf call_zopen64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf file,int mode)); +int call_zseek64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin)); +ZPOS64_T call_ztell64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf filestream)); + +void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32); + +#define ZOPEN64(filefunc,filename,mode) (call_zopen64((&(filefunc)),(filename),(mode))) +#define ZTELL64(filefunc,filestream) (call_ztell64((&(filefunc)),(filestream))) +#define ZSEEK64(filefunc,filestream,pos,mode) (call_zseek64((&(filefunc)),(filestream),(pos),(mode))) + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/ultimmc/libraries/quazip/quazip/qioapi.cpp b/ultimmc/libraries/quazip/quazip/qioapi.cpp new file mode 100644 index 0000000..641883b --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/qioapi.cpp @@ -0,0 +1,363 @@ +/* ioapi.c -- IO base function header for compress/uncompress .zip + files using zlib + zip or unzip API + + Version 1.01e, February 12th, 2005 + + Copyright (C) 1998-2005 Gilles Vollant + + Modified by Sergey A. Tachenov to integrate with Qt. +*/ + +#include +#include +#include + +#include "zlib.h" +#include "ioapi.h" +#include "quazip_global.h" +#include +#if (QT_VERSION >= 0x050100) +#define QUAZIP_QSAVEFILE_BUG_WORKAROUND +#endif +#ifdef QUAZIP_QSAVEFILE_BUG_WORKAROUND +#include +#endif + +/* I've found an old Unix (a SunOS 4.1.3_U1) without all SEEK_* defined.... */ + +#ifndef SEEK_CUR +#define SEEK_CUR 1 +#endif + +#ifndef SEEK_END +#define SEEK_END 2 +#endif + +#ifndef SEEK_SET +#define SEEK_SET 0 +#endif + +voidpf call_zopen64 (const zlib_filefunc64_32_def* pfilefunc,voidpf file,int mode) +{ + if (pfilefunc->zfile_func64.zopen64_file != NULL) + return (*(pfilefunc->zfile_func64.zopen64_file)) (pfilefunc->zfile_func64.opaque,file,mode); + else + { + return (*(pfilefunc->zopen32_file))(pfilefunc->zfile_func64.opaque,file,mode); + } +} + +int call_zseek64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin) +{ + if (pfilefunc->zfile_func64.zseek64_file != NULL) + return (*(pfilefunc->zfile_func64.zseek64_file)) (pfilefunc->zfile_func64.opaque,filestream,offset,origin); + else + { + uLong offsetTruncated = (uLong)offset; + if (offsetTruncated != offset) + return -1; + else + return (*(pfilefunc->zseek32_file))(pfilefunc->zfile_func64.opaque,filestream,offsetTruncated,origin); + } +} + +ZPOS64_T call_ztell64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream) +{ + if (pfilefunc->zfile_func64.zseek64_file != NULL) + return (*(pfilefunc->zfile_func64.ztell64_file)) (pfilefunc->zfile_func64.opaque,filestream); + else + { + uLong tell_uLong = (*(pfilefunc->ztell32_file))(pfilefunc->zfile_func64.opaque,filestream); + if ((tell_uLong) == ((uLong)-1)) + return (ZPOS64_T)-1; + else + return tell_uLong; + } +} + +/// @cond internal +struct QIODevice_descriptor { + // Position only used for writing to sequential devices. + qint64 pos; + inline QIODevice_descriptor(): + pos(0) + {} +}; +/// @endcond + +voidpf ZCALLBACK qiodevice_open_file_func ( + voidpf opaque, + voidpf file, + int mode) +{ + QIODevice_descriptor *d = reinterpret_cast(opaque); + QIODevice *iodevice = reinterpret_cast(file); + QIODevice::OpenMode desiredMode; + if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ) + desiredMode = QIODevice::ReadOnly; + else if (mode & ZLIB_FILEFUNC_MODE_EXISTING) + desiredMode = QIODevice::ReadWrite; + else if (mode & ZLIB_FILEFUNC_MODE_CREATE) + desiredMode = QIODevice::WriteOnly; + if (iodevice->isOpen()) { + if ((iodevice->openMode() & desiredMode) == desiredMode) { + if (desiredMode != QIODevice::WriteOnly + && iodevice->isSequential()) { + // We can use sequential devices only for writing. + delete d; + return NULL; + } else { + if ((desiredMode & QIODevice::WriteOnly) != 0) { + // open for writing, need to seek existing device + if (!iodevice->isSequential()) { + iodevice->seek(0); + } else { + d->pos = iodevice->pos(); + } + } + } + return iodevice; + } else { + delete d; + return NULL; + } + } + iodevice->open(desiredMode); + if (iodevice->isOpen()) { + if (desiredMode != QIODevice::WriteOnly && iodevice->isSequential()) { + // We can use sequential devices only for writing. + iodevice->close(); + delete d; + return NULL; + } else { + return iodevice; + } + } else { + delete d; + return NULL; + } +} + + +uLong ZCALLBACK qiodevice_read_file_func ( + voidpf opaque, + voidpf stream, + void* buf, + uLong size) +{ + QIODevice_descriptor *d = reinterpret_cast(opaque); + QIODevice *iodevice = reinterpret_cast(stream); + qint64 ret64 = iodevice->read((char*)buf,size); + uLong ret; + ret = (uLong) ret64; + if (ret64 != -1) { + d->pos += ret64; + } + return ret; +} + + +uLong ZCALLBACK qiodevice_write_file_func ( + voidpf opaque, + voidpf stream, + const void* buf, + uLong size) +{ + QIODevice_descriptor *d = reinterpret_cast(opaque); + QIODevice *iodevice = reinterpret_cast(stream); + uLong ret; + qint64 ret64 = iodevice->write((char*)buf,size); + if (ret64 != -1) { + d->pos += ret64; + } + ret = (uLong) ret64; + return ret; +} + +uLong ZCALLBACK qiodevice_tell_file_func ( + voidpf opaque, + voidpf stream) +{ + QIODevice_descriptor *d = reinterpret_cast(opaque); + QIODevice *iodevice = reinterpret_cast(stream); + uLong ret; + qint64 ret64; + if (iodevice->isSequential()) { + ret64 = d->pos; + } else { + ret64 = iodevice->pos(); + } + ret = static_cast(ret64); + return ret; +} + +ZPOS64_T ZCALLBACK qiodevice64_tell_file_func ( + voidpf opaque, + voidpf stream) +{ + QIODevice_descriptor *d = reinterpret_cast(opaque); + QIODevice *iodevice = reinterpret_cast(stream); + qint64 ret; + if (iodevice->isSequential()) { + ret = d->pos; + } else { + ret = iodevice->pos(); + } + return static_cast(ret); +} + +int ZCALLBACK qiodevice_seek_file_func ( + voidpf /*opaque UNUSED*/, + voidpf stream, + uLong offset, + int origin) +{ + QIODevice *iodevice = reinterpret_cast(stream); + if (iodevice->isSequential()) { + if (origin == ZLIB_FILEFUNC_SEEK_END + && offset == 0) { + // sequential devices are always at end (needed in mdAppend) + return 0; + } else { + qWarning("qiodevice_seek_file_func() called for sequential device"); + return -1; + } + } + uLong qiodevice_seek_result=0; + int ret; + switch (origin) + { + case ZLIB_FILEFUNC_SEEK_CUR : + qiodevice_seek_result = ((QIODevice*)stream)->pos() + offset; + break; + case ZLIB_FILEFUNC_SEEK_END : + qiodevice_seek_result = ((QIODevice*)stream)->size() - offset; + break; + case ZLIB_FILEFUNC_SEEK_SET : + qiodevice_seek_result = offset; + break; + default: + return -1; + } + ret = !iodevice->seek(qiodevice_seek_result); + return ret; +} + +int ZCALLBACK qiodevice64_seek_file_func ( + voidpf /*opaque UNUSED*/, + voidpf stream, + ZPOS64_T offset, + int origin) +{ + QIODevice *iodevice = reinterpret_cast(stream); + if (iodevice->isSequential()) { + if (origin == ZLIB_FILEFUNC_SEEK_END + && offset == 0) { + // sequential devices are always at end (needed in mdAppend) + return 0; + } else { + qWarning("qiodevice_seek_file_func() called for sequential device"); + return -1; + } + } + qint64 qiodevice_seek_result=0; + int ret; + switch (origin) + { + case ZLIB_FILEFUNC_SEEK_CUR : + qiodevice_seek_result = ((QIODevice*)stream)->pos() + offset; + break; + case ZLIB_FILEFUNC_SEEK_END : + qiodevice_seek_result = ((QIODevice*)stream)->size() - offset; + break; + case ZLIB_FILEFUNC_SEEK_SET : + qiodevice_seek_result = offset; + break; + default: + return -1; + } + ret = !iodevice->seek(qiodevice_seek_result); + return ret; +} + +int ZCALLBACK qiodevice_close_file_func ( + voidpf opaque, + voidpf stream) +{ + QIODevice_descriptor *d = reinterpret_cast(opaque); + delete d; + QIODevice *device = reinterpret_cast(stream); +#ifdef QUAZIP_QSAVEFILE_BUG_WORKAROUND + // QSaveFile terribly breaks the is-a idiom: + // it IS a QIODevice, but it is NOT compatible with it: close() is private + QSaveFile *file = qobject_cast(device); + if (file != NULL) { + // We have to call the ugly commit() instead: + return file->commit() ? 0 : -1; + } +#endif + device->close(); + return 0; +} + +int ZCALLBACK qiodevice_fakeclose_file_func ( + voidpf opaque, + voidpf /*stream*/) +{ + QIODevice_descriptor *d = reinterpret_cast(opaque); + delete d; + return 0; +} + +int ZCALLBACK qiodevice_error_file_func ( + voidpf /*opaque UNUSED*/, + voidpf /*stream UNUSED*/) +{ + // can't check for error due to the QIODevice API limitation + return 0; +} + +void fill_qiodevice_filefunc ( + zlib_filefunc_def* pzlib_filefunc_def) +{ + pzlib_filefunc_def->zopen_file = qiodevice_open_file_func; + pzlib_filefunc_def->zread_file = qiodevice_read_file_func; + pzlib_filefunc_def->zwrite_file = qiodevice_write_file_func; + pzlib_filefunc_def->ztell_file = qiodevice_tell_file_func; + pzlib_filefunc_def->zseek_file = qiodevice_seek_file_func; + pzlib_filefunc_def->zclose_file = qiodevice_close_file_func; + pzlib_filefunc_def->zerror_file = qiodevice_error_file_func; + pzlib_filefunc_def->opaque = new QIODevice_descriptor; +} + +void fill_qiodevice64_filefunc ( + zlib_filefunc64_def* pzlib_filefunc_def) +{ + // Open functions are the same for Qt. + pzlib_filefunc_def->zopen64_file = qiodevice_open_file_func; + pzlib_filefunc_def->zread_file = qiodevice_read_file_func; + pzlib_filefunc_def->zwrite_file = qiodevice_write_file_func; + pzlib_filefunc_def->ztell64_file = qiodevice64_tell_file_func; + pzlib_filefunc_def->zseek64_file = qiodevice64_seek_file_func; + pzlib_filefunc_def->zclose_file = qiodevice_close_file_func; + pzlib_filefunc_def->zerror_file = qiodevice_error_file_func; + pzlib_filefunc_def->opaque = new QIODevice_descriptor; + pzlib_filefunc_def->zfakeclose_file = qiodevice_fakeclose_file_func; +} + +void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32) +{ + p_filefunc64_32->zfile_func64.zopen64_file = NULL; + p_filefunc64_32->zopen32_file = p_filefunc32->zopen_file; + p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file; + p_filefunc64_32->zfile_func64.zread_file = p_filefunc32->zread_file; + p_filefunc64_32->zfile_func64.zwrite_file = p_filefunc32->zwrite_file; + p_filefunc64_32->zfile_func64.ztell64_file = NULL; + p_filefunc64_32->zfile_func64.zseek64_file = NULL; + p_filefunc64_32->zfile_func64.zclose_file = p_filefunc32->zclose_file; + p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file; + p_filefunc64_32->zfile_func64.opaque = p_filefunc32->opaque; + p_filefunc64_32->zfile_func64.zfakeclose_file = NULL; + p_filefunc64_32->zseek32_file = p_filefunc32->zseek_file; + p_filefunc64_32->ztell32_file = p_filefunc32->ztell_file; +} diff --git a/ultimmc/libraries/quazip/quazip/quaadler32.cpp b/ultimmc/libraries/quazip/quazip/quaadler32.cpp new file mode 100644 index 0000000..161db11 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quaadler32.cpp @@ -0,0 +1,53 @@ +/* +Copyright (C) 2010 Adam Walczak +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include "quaadler32.h" + +#include "zlib.h" + +QuaAdler32::QuaAdler32() +{ + reset(); +} + +quint32 QuaAdler32::calculate(const QByteArray &data) +{ + return adler32( adler32(0L, Z_NULL, 0), (const Bytef*)data.data(), data.size() ); +} + +void QuaAdler32::reset() +{ + checksum = adler32(0L, Z_NULL, 0); +} + +void QuaAdler32::update(const QByteArray &buf) +{ + checksum = adler32( checksum, (const Bytef*)buf.data(), buf.size() ); +} + +quint32 QuaAdler32::value() +{ + return checksum; +} diff --git a/ultimmc/libraries/quazip/quazip/quaadler32.h b/ultimmc/libraries/quazip/quazip/quaadler32.h new file mode 100644 index 0000000..e8847f4 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quaadler32.h @@ -0,0 +1,54 @@ +#ifndef QUAADLER32_H +#define QUAADLER32_H + +/* +Copyright (C) 2010 Adam Walczak +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include + +#include "quachecksum32.h" + +/// Adler32 checksum +/** \class QuaAdler32 quaadler32.h + * This class wrappers the adler32 function with the QuaChecksum32 interface. + * See QuaChecksum32 for more info. + */ +class QUAZIP_EXPORT QuaAdler32 : public QuaChecksum32 +{ + +public: + QuaAdler32(); + + quint32 calculate(const QByteArray &data); + + void reset(); + void update(const QByteArray &buf); + quint32 value(); + +private: + quint32 checksum; +}; + +#endif //QUAADLER32_H diff --git a/ultimmc/libraries/quazip/quazip/quachecksum32.h b/ultimmc/libraries/quazip/quazip/quachecksum32.h new file mode 100644 index 0000000..40ff451 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quachecksum32.h @@ -0,0 +1,78 @@ +#ifndef QUACHECKSUM32_H +#define QUACHECKSUM32_H + +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include +#include "quazip_global.h" + +/// Checksum interface. +/** \class QuaChecksum32 quachecksum32.h + * This is an interface for 32 bit checksums. + * Classes implementing this interface can calcunate a certin + * checksum in a single step: + * \code + * QChecksum32 *crc32 = new QuaCrc32(); + * rasoult = crc32->calculate(data); + * \endcode + * or by streaming the data: + * \code + * QChecksum32 *crc32 = new QuaCrc32(); + * while(!fileA.atEnd()) + * crc32->update(fileA.read(bufSize)); + * resoultA = crc32->value(); + * crc32->reset(); + * while(!fileB.atEnd()) + * crc32->update(fileB.read(bufSize)); + * resoultB = crc32->value(); + * \endcode + */ +class QUAZIP_EXPORT QuaChecksum32 +{ + +public: + ///Calculates the checksum for data. + /** \a data source data + * \return data checksum + * + * This function has no efect on the value returned by value(). + */ + virtual quint32 calculate(const QByteArray &data) = 0; + + ///Resets the calculation on a checksun for a stream. + virtual void reset() = 0; + + ///Updates the calculated checksum for the stream + /** \a buf next portion of data from the stream + */ + virtual void update(const QByteArray &buf) = 0; + + ///Value of the checksum calculated for the stream passed throw update(). + /** \return checksum + */ + virtual quint32 value() = 0; +}; + +#endif //QUACHECKSUM32_H diff --git a/ultimmc/libraries/quazip/quazip/quacrc32.cpp b/ultimmc/libraries/quazip/quazip/quacrc32.cpp new file mode 100644 index 0000000..2de5117 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quacrc32.cpp @@ -0,0 +1,52 @@ +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include "quacrc32.h" + +#include "zlib.h" + +QuaCrc32::QuaCrc32() +{ + reset(); +} + +quint32 QuaCrc32::calculate(const QByteArray &data) +{ + return crc32( crc32(0L, Z_NULL, 0), (const Bytef*)data.data(), data.size() ); +} + +void QuaCrc32::reset() +{ + checksum = crc32(0L, Z_NULL, 0); +} + +void QuaCrc32::update(const QByteArray &buf) +{ + checksum = crc32( checksum, (const Bytef*)buf.data(), buf.size() ); +} + +quint32 QuaCrc32::value() +{ + return checksum; +} diff --git a/ultimmc/libraries/quazip/quazip/quacrc32.h b/ultimmc/libraries/quazip/quazip/quacrc32.h new file mode 100644 index 0000000..af7703b --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quacrc32.h @@ -0,0 +1,50 @@ +#ifndef QUACRC32_H +#define QUACRC32_H + +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include "quachecksum32.h" + +///CRC32 checksum +/** \class QuaCrc32 quacrc32.h +* This class wrappers the crc32 function with the QuaChecksum32 interface. +* See QuaChecksum32 for more info. +*/ +class QUAZIP_EXPORT QuaCrc32 : public QuaChecksum32 { + +public: + QuaCrc32(); + + quint32 calculate(const QByteArray &data); + + void reset(); + void update(const QByteArray &buf); + quint32 value(); + +private: + quint32 checksum; +}; + +#endif //QUACRC32_H diff --git a/ultimmc/libraries/quazip/quazip/quagzipfile.cpp b/ultimmc/libraries/quazip/quazip/quagzipfile.cpp new file mode 100644 index 0000000..efc54c1 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quagzipfile.cpp @@ -0,0 +1,172 @@ +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include + +#include "quagzipfile.h" + +/// \cond internal +class QuaGzipFilePrivate { + friend class QuaGzipFile; + QString fileName; + gzFile gzd; + inline QuaGzipFilePrivate(): gzd(NULL) {} + inline QuaGzipFilePrivate(const QString &fileName): + fileName(fileName), gzd(NULL) {} + template bool open(FileId id, + QIODevice::OpenMode mode, QString &error); + gzFile open(int fd, const char *modeString); + gzFile open(const QString &name, const char *modeString); +}; + +gzFile QuaGzipFilePrivate::open(const QString &name, const char *modeString) +{ + return gzopen(QFile::encodeName(name).constData(), modeString); +} + +gzFile QuaGzipFilePrivate::open(int fd, const char *modeString) +{ + return gzdopen(fd, modeString); +} + +template +bool QuaGzipFilePrivate::open(FileId id, QIODevice::OpenMode mode, + QString &error) +{ + char modeString[2]; + modeString[0] = modeString[1] = '\0'; + if ((mode & QIODevice::Append) != 0) { + error = QuaGzipFile::trUtf8("QIODevice::Append is not " + "supported for GZIP"); + return false; + } + if ((mode & QIODevice::ReadOnly) != 0 + && (mode & QIODevice::WriteOnly) != 0) { + error = QuaGzipFile::trUtf8("Opening gzip for both reading" + " and writing is not supported"); + return false; + } else if ((mode & QIODevice::ReadOnly) != 0) { + modeString[0] = 'r'; + } else if ((mode & QIODevice::WriteOnly) != 0) { + modeString[0] = 'w'; + } else { + error = QuaGzipFile::trUtf8("You can open a gzip either for reading" + " or for writing. Which is it?"); + return false; + } + gzd = open(id, modeString); + if (gzd == NULL) { + error = QuaGzipFile::trUtf8("Could not gzopen() file"); + return false; + } + return true; +} +/// \endcond + +QuaGzipFile::QuaGzipFile(): +d(new QuaGzipFilePrivate()) +{ +} + +QuaGzipFile::QuaGzipFile(QObject *parent): +QIODevice(parent), +d(new QuaGzipFilePrivate()) +{ +} + +QuaGzipFile::QuaGzipFile(const QString &fileName, QObject *parent): + QIODevice(parent), +d(new QuaGzipFilePrivate(fileName)) +{ +} + +QuaGzipFile::~QuaGzipFile() +{ + if (isOpen()) { + close(); + } + delete d; +} + +void QuaGzipFile::setFileName(const QString& fileName) +{ + d->fileName = fileName; +} + +QString QuaGzipFile::getFileName() const +{ + return d->fileName; +} + +bool QuaGzipFile::isSequential() const +{ + return true; +} + +bool QuaGzipFile::open(QIODevice::OpenMode mode) +{ + QString error; + if (!d->open(d->fileName, mode, error)) { + setErrorString(error); + return false; + } + return QIODevice::open(mode); +} + +bool QuaGzipFile::open(int fd, QIODevice::OpenMode mode) +{ + QString error; + if (!d->open(fd, mode, error)) { + setErrorString(error); + return false; + } + return QIODevice::open(mode); +} + +bool QuaGzipFile::flush() +{ + return gzflush(d->gzd, Z_SYNC_FLUSH) == Z_OK; +} + +void QuaGzipFile::close() +{ + QIODevice::close(); + gzclose(d->gzd); +} + +qint64 QuaGzipFile::readData(char *data, qint64 maxSize) +{ + return gzread(d->gzd, (voidp)data, (unsigned)maxSize); +} + +qint64 QuaGzipFile::writeData(const char *data, qint64 maxSize) +{ + if (maxSize == 0) + return 0; + int written = gzwrite(d->gzd, (voidp)data, (unsigned)maxSize); + if (written == 0) + return -1; + else + return written; +} diff --git a/ultimmc/libraries/quazip/quazip/quagzipfile.h b/ultimmc/libraries/quazip/quazip/quagzipfile.h new file mode 100644 index 0000000..e2f9d97 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quagzipfile.h @@ -0,0 +1,108 @@ +#ifndef QUAZIP_QUAGZIPFILE_H +#define QUAZIP_QUAGZIPFILE_H + +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include +#include "quazip_global.h" + +#include + +class QuaGzipFilePrivate; + +/// GZIP file +/** + This class is a wrapper around GZIP file access functions in zlib. Unlike QuaZip classes, it doesn't allow reading from a GZIP file opened as QIODevice, for example, if your GZIP file is in QBuffer. It only provides QIODevice access to a GZIP file contents, but the GZIP file itself must be identified by its name on disk or by descriptor id. + */ +class QUAZIP_EXPORT QuaGzipFile: public QIODevice { + Q_OBJECT +public: + /// Empty constructor. + /** + Must call setFileName() before trying to open. + */ + QuaGzipFile(); + /// Empty constructor with a parent. + /** + Must call setFileName() before trying to open. + \param parent The parent object, as per QObject logic. + */ + QuaGzipFile(QObject *parent); + /// Constructor. + /** + \param fileName The name of the GZIP file. + \param parent The parent object, as per QObject logic. + */ + QuaGzipFile(const QString &fileName, QObject *parent = NULL); + /// Destructor. + virtual ~QuaGzipFile(); + /// Sets the name of the GZIP file to be opened. + void setFileName(const QString& fileName); + /// Returns the name of the GZIP file. + QString getFileName() const; + /// Returns true. + /** + Strictly speaking, zlib supports seeking for GZIP files, but it is + poorly implemented, because there is no way to implement it + properly. For reading, seeking backwards is very slow, and for + writing, it is downright impossible. Therefore, QuaGzipFile does not + support seeking at all. + */ + virtual bool isSequential() const; + /// Opens the file. + /** + \param mode Can be either QIODevice::Write or QIODevice::Read. + ReadWrite and Append aren't supported. + */ + virtual bool open(QIODevice::OpenMode mode); + /// Opens the file. + /** + \overload + \param fd The file descriptor to read/write the GZIP file from/to. + \param mode Can be either QIODevice::Write or QIODevice::Read. + ReadWrite and Append aren't supported. + */ + virtual bool open(int fd, QIODevice::OpenMode mode); + /// Flushes data to file. + /** + The data is written using Z_SYNC_FLUSH mode. Doesn't make any sense + when reading. + */ + virtual bool flush(); + /// Closes the file. + virtual void close(); +protected: + /// Implementation of QIODevice::readData(). + virtual qint64 readData(char *data, qint64 maxSize); + /// Implementation of QIODevice::writeData(). + virtual qint64 writeData(const char *data, qint64 maxSize); +private: + // not implemented by design to disable copy + QuaGzipFile(const QuaGzipFile &that); + QuaGzipFile& operator=(const QuaGzipFile &that); + QuaGzipFilePrivate *d; +}; + +#endif // QUAZIP_QUAGZIPFILE_H diff --git a/ultimmc/libraries/quazip/quazip/quaziodevice.cpp b/ultimmc/libraries/quazip/quazip/quaziodevice.cpp new file mode 100644 index 0000000..04f34bf --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quaziodevice.cpp @@ -0,0 +1,339 @@ +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include "quaziodevice.h" + +#define QUAZIO_INBUFSIZE 4096 +#define QUAZIO_OUTBUFSIZE 4096 + +/// \cond internal +class QuaZIODevicePrivate { + friend class QuaZIODevice; + QuaZIODevicePrivate(QIODevice *io); + ~QuaZIODevicePrivate(); + QIODevice *io; + z_stream zins; + z_stream zouts; + char *inBuf; + int inBufPos; + int inBufSize; + char *outBuf; + int outBufPos; + int outBufSize; + bool zBufError; + bool atEnd; + int doFlush(QString &error); +}; + +QuaZIODevicePrivate::QuaZIODevicePrivate(QIODevice *io): + io(io), + inBuf(NULL), + inBufPos(0), + inBufSize(0), + outBuf(NULL), + outBufPos(0), + outBufSize(0), + zBufError(false), + atEnd(false) +{ + zins.zalloc = (alloc_func) NULL; + zins.zfree = (free_func) NULL; + zins.opaque = NULL; + zouts.zalloc = (alloc_func) NULL; + zouts.zfree = (free_func) NULL; + zouts.opaque = NULL; + inBuf = new char[QUAZIO_INBUFSIZE]; + outBuf = new char[QUAZIO_OUTBUFSIZE]; +#ifdef QUAZIP_ZIODEVICE_DEBUG_OUTPUT + debug.setFileName("debug.out"); + debug.open(QIODevice::WriteOnly); +#endif +#ifdef QUAZIP_ZIODEVICE_DEBUG_INPUT + indebug.setFileName("debug.in"); + indebug.open(QIODevice::WriteOnly); +#endif +} + +QuaZIODevicePrivate::~QuaZIODevicePrivate() +{ +#ifdef QUAZIP_ZIODEVICE_DEBUG_OUTPUT + debug.close(); +#endif +#ifdef QUAZIP_ZIODEVICE_DEBUG_INPUT + indebug.close(); +#endif + if (inBuf != NULL) + delete[] inBuf; + if (outBuf != NULL) + delete[] outBuf; +} + +int QuaZIODevicePrivate::doFlush(QString &error) +{ + int flushed = 0; + while (outBufPos < outBufSize) { + int more = io->write(outBuf + outBufPos, outBufSize - outBufPos); + if (more == -1) { + error = io->errorString(); + return -1; + } + if (more == 0) + break; + outBufPos += more; + flushed += more; + } + if (outBufPos == outBufSize) { + outBufPos = outBufSize = 0; + } + return flushed; +} + +/// \endcond + +// #define QUAZIP_ZIODEVICE_DEBUG_OUTPUT +// #define QUAZIP_ZIODEVICE_DEBUG_INPUT +#ifdef QUAZIP_ZIODEVICE_DEBUG_OUTPUT +#include +static QFile debug; +#endif +#ifdef QUAZIP_ZIODEVICE_DEBUG_INPUT +#include +static QFile indebug; +#endif + +QuaZIODevice::QuaZIODevice(QIODevice *io, QObject *parent): + QIODevice(parent), + d(new QuaZIODevicePrivate(io)) +{ + connect(io, SIGNAL(readyRead()), SIGNAL(readyRead())); +} + +QuaZIODevice::~QuaZIODevice() +{ + if (isOpen()) + close(); + delete d; +} + +QIODevice *QuaZIODevice::getIoDevice() const +{ + return d->io; +} + +bool QuaZIODevice::open(QIODevice::OpenMode mode) +{ + if ((mode & QIODevice::Append) != 0) { + setErrorString(trUtf8("QIODevice::Append is not supported for" + " QuaZIODevice")); + return false; + } + if ((mode & QIODevice::ReadWrite) == QIODevice::ReadWrite) { + setErrorString(trUtf8("QIODevice::ReadWrite is not supported for" + " QuaZIODevice")); + return false; + } + if ((mode & QIODevice::ReadOnly) != 0) { + if (inflateInit(&d->zins) != Z_OK) { + setErrorString(d->zins.msg); + return false; + } + } + if ((mode & QIODevice::WriteOnly) != 0) { + if (deflateInit(&d->zouts, Z_DEFAULT_COMPRESSION) != Z_OK) { + setErrorString(d->zouts.msg); + return false; + } + } + return QIODevice::open(mode); +} + +void QuaZIODevice::close() +{ + if ((openMode() & QIODevice::ReadOnly) != 0) { + if (inflateEnd(&d->zins) != Z_OK) { + setErrorString(d->zins.msg); + } + } + if ((openMode() & QIODevice::WriteOnly) != 0) { + flush(); + if (deflateEnd(&d->zouts) != Z_OK) { + setErrorString(d->zouts.msg); + } + } + QIODevice::close(); +} + +qint64 QuaZIODevice::readData(char *data, qint64 maxSize) +{ + int read = 0; + while (read < maxSize) { + if (d->inBufPos == d->inBufSize) { + d->inBufPos = 0; + d->inBufSize = d->io->read(d->inBuf, QUAZIO_INBUFSIZE); + if (d->inBufSize == -1) { + d->inBufSize = 0; + setErrorString(d->io->errorString()); + return -1; + } + if (d->inBufSize == 0) + break; + } + while (read < maxSize && d->inBufPos < d->inBufSize) { + d->zins.next_in = (Bytef *) (d->inBuf + d->inBufPos); + d->zins.avail_in = d->inBufSize - d->inBufPos; + d->zins.next_out = (Bytef *) (data + read); + d->zins.avail_out = (uInt) (maxSize - read); // hope it's less than 2GB + int more = 0; + switch (inflate(&d->zins, Z_SYNC_FLUSH)) { + case Z_OK: + read = (char *) d->zins.next_out - data; + d->inBufPos = (char *) d->zins.next_in - d->inBuf; + break; + case Z_STREAM_END: + read = (char *) d->zins.next_out - data; + d->inBufPos = (char *) d->zins.next_in - d->inBuf; + d->atEnd = true; + return read; + case Z_BUF_ERROR: // this should never happen, but just in case + if (!d->zBufError) { + qWarning("Z_BUF_ERROR detected with %d/%d in/out, weird", + d->zins.avail_in, d->zins.avail_out); + d->zBufError = true; + } + memmove(d->inBuf, d->inBuf + d->inBufPos, d->inBufSize - d->inBufPos); + d->inBufSize -= d->inBufPos; + d->inBufPos = 0; + more = d->io->read(d->inBuf + d->inBufSize, QUAZIO_INBUFSIZE - d->inBufSize); + if (more == -1) { + setErrorString(d->io->errorString()); + return -1; + } + if (more == 0) + return read; + d->inBufSize += more; + break; + default: + setErrorString(QString::fromLocal8Bit(d->zins.msg)); + return -1; + } + } + } +#ifdef QUAZIP_ZIODEVICE_DEBUG_INPUT + indebug.write(data, read); +#endif + return read; +} + +qint64 QuaZIODevice::writeData(const char *data, qint64 maxSize) +{ + int written = 0; + QString error; + if (d->doFlush(error) == -1) { + setErrorString(error); + return -1; + } + while (written < maxSize) { + // there is some data waiting in the output buffer + if (d->outBufPos < d->outBufSize) + return written; + d->zouts.next_in = (Bytef *) (data + written); + d->zouts.avail_in = (uInt) (maxSize - written); // hope it's less than 2GB + d->zouts.next_out = (Bytef *) d->outBuf; + d->zouts.avail_out = QUAZIO_OUTBUFSIZE; + switch (deflate(&d->zouts, Z_NO_FLUSH)) { + case Z_OK: + written = (char *) d->zouts.next_in - data; + d->outBufSize = (char *) d->zouts.next_out - d->outBuf; + break; + default: + setErrorString(QString::fromLocal8Bit(d->zouts.msg)); + return -1; + } + if (d->doFlush(error) == -1) { + setErrorString(error); + return -1; + } + } +#ifdef QUAZIP_ZIODEVICE_DEBUG_OUTPUT + debug.write(data, written); +#endif + return written; +} + +bool QuaZIODevice::flush() +{ + QString error; + if (d->doFlush(error) < 0) { + setErrorString(error); + return false; + } + // can't flush buffer, some data is still waiting + if (d->outBufPos < d->outBufSize) + return true; + Bytef c = 0; + d->zouts.next_in = &c; // fake input buffer + d->zouts.avail_in = 0; // of zero size + do { + d->zouts.next_out = (Bytef *) d->outBuf; + d->zouts.avail_out = QUAZIO_OUTBUFSIZE; + switch (deflate(&d->zouts, Z_SYNC_FLUSH)) { + case Z_OK: + d->outBufSize = (char *) d->zouts.next_out - d->outBuf; + if (d->doFlush(error) < 0) { + setErrorString(error); + return false; + } + if (d->outBufPos < d->outBufSize) + return true; + break; + case Z_BUF_ERROR: // nothing to write? + return true; + default: + setErrorString(QString::fromLocal8Bit(d->zouts.msg)); + return false; + } + } while (d->zouts.avail_out == 0); + return true; +} + +bool QuaZIODevice::isSequential() const +{ + return true; +} + +bool QuaZIODevice::atEnd() const +{ + // Here we MUST check QIODevice::bytesAvailable() because WE + // might have reached the end, but QIODevice didn't-- + // it could have simply pre-buffered all remaining data. + return (openMode() == NotOpen) || (QIODevice::bytesAvailable() == 0 && d->atEnd); +} + +qint64 QuaZIODevice::bytesAvailable() const +{ + // If we haven't recevied Z_STREAM_END, it means that + // we have at least one more input byte available. + // Plus whatever QIODevice has buffered. + return (d->atEnd ? 0 : 1) + QIODevice::bytesAvailable(); +} diff --git a/ultimmc/libraries/quazip/quazip/quaziodevice.h b/ultimmc/libraries/quazip/quazip/quaziodevice.h new file mode 100644 index 0000000..63499c3 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quaziodevice.h @@ -0,0 +1,102 @@ +#ifndef QUAZIP_QUAZIODEVICE_H +#define QUAZIP_QUAZIODEVICE_H + +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include +#include "quazip_global.h" + +#include + +class QuaZIODevicePrivate; + +/// A class to compress/decompress QIODevice. +/** + This class can be used to compress any data written to QIODevice or + decompress it back. Compressing data sent over a QTcpSocket is a good + example. + */ +class QUAZIP_EXPORT QuaZIODevice: public QIODevice { + Q_OBJECT +public: + /// Constructor. + /** + \param io The QIODevice to read/write. + \param parent The parent object, as per QObject logic. + */ + QuaZIODevice(QIODevice *io, QObject *parent = NULL); + /// Destructor. + ~QuaZIODevice(); + /// Flushes data waiting to be written. + /** + Unfortunately, as QIODevice doesn't support flush() by itself, the + only thing this method does is write the compressed data into the + device using Z_SYNC_FLUSH mode. If you need the compressed data to + actually be flushed from the buffer of the underlying QIODevice, you + need to call its flush() method as well, providing it supports it + (like QTcpSocket does). Example: + \code + QuaZIODevice dev(&sock); + dev.open(QIODevice::Write); + dev.write(yourDataGoesHere); + dev.flush(); + sock->flush(); // this actually sends data to network + \endcode + + This may change in the future versions of QuaZIP by implementing an + ugly hack: trying to cast the QIODevice using qobject_cast to known + flush()-supporting subclasses, and calling flush if the resulting + pointer is not zero. + */ + virtual bool flush(); + /// Opens the device. + /** + \param mode Neither QIODevice::ReadWrite nor QIODevice::Append are + not supported. + */ + virtual bool open(QIODevice::OpenMode mode); + /// Closes this device, but not the underlying one. + /** + The underlying QIODevice is not closed in case you want to write + something else to it. + */ + virtual void close(); + /// Returns the underlying device. + QIODevice *getIoDevice() const; + /// Returns true. + virtual bool isSequential() const; + /// Returns true iff the end of the compressed stream is reached. + virtual bool atEnd() const; + /// Returns the number of the bytes buffered. + virtual qint64 bytesAvailable() const; +protected: + /// Implementation of QIODevice::readData(). + virtual qint64 readData(char *data, qint64 maxSize); + /// Implementation of QIODevice::writeData(). + virtual qint64 writeData(const char *data, qint64 maxSize); +private: + QuaZIODevicePrivate *d; +}; +#endif // QUAZIP_QUAZIODEVICE_H diff --git a/ultimmc/libraries/quazip/quazip/quazip.cpp b/ultimmc/libraries/quazip/quazip/quazip.cpp new file mode 100644 index 0000000..a5bc44e --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quazip.cpp @@ -0,0 +1,795 @@ +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant, see +quazip/(un)zip.h files for details, basically it's zlib license. + **/ + +#include +#include +#include + +#include "quazip.h" + +/// All the internal stuff for the QuaZip class. +/** + \internal + + This class keeps all the private stuff for the QuaZip class so it can + be changed without breaking binary compatibility, according to the + Pimpl idiom. + */ +class QuaZipPrivate { + friend class QuaZip; + private: + Q_DISABLE_COPY(QuaZipPrivate) + /// The pointer to the corresponding QuaZip instance. + QuaZip *q; + /// The codec for file names. + QTextCodec *fileNameCodec; + /// The codec for comments. + QTextCodec *commentCodec; + /// The archive file name. + QString zipName; + /// The device to access the archive. + QIODevice *ioDevice; + /// The global comment. + QString comment; + /// The open mode. + QuaZip::Mode mode; + union { + /// The internal handle for UNZIP modes. + unzFile unzFile_f; + /// The internal handle for ZIP modes. + zipFile zipFile_f; + }; + /// Whether a current file is set. + bool hasCurrentFile_f; + /// The last error. + int zipError; + /// Whether \ref QuaZip::setDataDescriptorWritingEnabled() "the data descriptor writing mode" is enabled. + bool dataDescriptorWritingEnabled; + /// The zip64 mode. + bool zip64; + /// The auto-close flag. + bool autoClose; + inline QTextCodec *getDefaultFileNameCodec() + { + if (defaultFileNameCodec == NULL) { + return QTextCodec::codecForLocale(); + } else { + return defaultFileNameCodec; + } + } + /// The constructor for the corresponding QuaZip constructor. + inline QuaZipPrivate(QuaZip *q): + q(q), + fileNameCodec(getDefaultFileNameCodec()), + commentCodec(QTextCodec::codecForLocale()), + ioDevice(NULL), + mode(QuaZip::mdNotOpen), + hasCurrentFile_f(false), + zipError(UNZ_OK), + dataDescriptorWritingEnabled(true), + zip64(false), + autoClose(true) + { + unzFile_f = NULL; + zipFile_f = NULL; + lastMappedDirectoryEntry.num_of_file = 0; + lastMappedDirectoryEntry.pos_in_zip_directory = 0; + } + /// The constructor for the corresponding QuaZip constructor. + inline QuaZipPrivate(QuaZip *q, const QString &zipName): + q(q), + fileNameCodec(getDefaultFileNameCodec()), + commentCodec(QTextCodec::codecForLocale()), + zipName(zipName), + ioDevice(NULL), + mode(QuaZip::mdNotOpen), + hasCurrentFile_f(false), + zipError(UNZ_OK), + dataDescriptorWritingEnabled(true), + zip64(false), + autoClose(true) + { + unzFile_f = NULL; + zipFile_f = NULL; + lastMappedDirectoryEntry.num_of_file = 0; + lastMappedDirectoryEntry.pos_in_zip_directory = 0; + } + /// The constructor for the corresponding QuaZip constructor. + inline QuaZipPrivate(QuaZip *q, QIODevice *ioDevice): + q(q), + fileNameCodec(getDefaultFileNameCodec()), + commentCodec(QTextCodec::codecForLocale()), + ioDevice(ioDevice), + mode(QuaZip::mdNotOpen), + hasCurrentFile_f(false), + zipError(UNZ_OK), + dataDescriptorWritingEnabled(true), + zip64(false), + autoClose(true) + { + unzFile_f = NULL; + zipFile_f = NULL; + lastMappedDirectoryEntry.num_of_file = 0; + lastMappedDirectoryEntry.pos_in_zip_directory = 0; + } + /// Returns either a list of file names or a list of QuaZipFileInfo. + template + bool getFileInfoList(QList *result) const; + + /// Stores map of filenames and file locations for unzipping + inline void clearDirectoryMap(); + inline void addCurrentFileToDirectoryMap(const QString &fileName); + bool goToFirstUnmappedFile(); + QHash directoryCaseSensitive; + QHash directoryCaseInsensitive; + unz64_file_pos lastMappedDirectoryEntry; + static QTextCodec *defaultFileNameCodec; +}; + +QTextCodec *QuaZipPrivate::defaultFileNameCodec = NULL; + +void QuaZipPrivate::clearDirectoryMap() +{ + directoryCaseInsensitive.clear(); + directoryCaseSensitive.clear(); + lastMappedDirectoryEntry.num_of_file = 0; + lastMappedDirectoryEntry.pos_in_zip_directory = 0; +} + +void QuaZipPrivate::addCurrentFileToDirectoryMap(const QString &fileName) +{ + if (!hasCurrentFile_f || fileName.isEmpty()) { + return; + } + // Adds current file to filename map as fileName + unz64_file_pos fileDirectoryPos; + unzGetFilePos64(unzFile_f, &fileDirectoryPos); + directoryCaseSensitive.insert(fileName, fileDirectoryPos); + // Only add lowercase to directory map if not already there + // ensures only map the first one seen + QString lower = fileName.toLower(); + if (!directoryCaseInsensitive.contains(lower)) + directoryCaseInsensitive.insert(lower, fileDirectoryPos); + // Mark last one + if (fileDirectoryPos.pos_in_zip_directory > lastMappedDirectoryEntry.pos_in_zip_directory) + lastMappedDirectoryEntry = fileDirectoryPos; +} + +bool QuaZipPrivate::goToFirstUnmappedFile() +{ + zipError = UNZ_OK; + if (mode != QuaZip::mdUnzip) { + qWarning("QuaZipPrivate::goToNextUnmappedFile(): ZIP is not open in mdUnzip mode"); + return false; + } + // If not mapped anything, go to beginning + if (lastMappedDirectoryEntry.pos_in_zip_directory == 0) { + unzGoToFirstFile(unzFile_f); + } else { + // Goto the last one mapped, plus one + unzGoToFilePos64(unzFile_f, &lastMappedDirectoryEntry); + unzGoToNextFile(unzFile_f); + } + hasCurrentFile_f=zipError==UNZ_OK; + if(zipError==UNZ_END_OF_LIST_OF_FILE) + zipError=UNZ_OK; + return hasCurrentFile_f; +} + +QuaZip::QuaZip(): + p(new QuaZipPrivate(this)) +{ +} + +QuaZip::QuaZip(const QString& zipName): + p(new QuaZipPrivate(this, zipName)) +{ +} + +QuaZip::QuaZip(QIODevice *ioDevice): + p(new QuaZipPrivate(this, ioDevice)) +{ +} + +QuaZip::~QuaZip() +{ + if(isOpen()) + close(); + delete p; +} + +bool QuaZip::open(Mode mode, zlib_filefunc_def* ioApi) +{ + p->zipError=UNZ_OK; + if(isOpen()) { + qWarning("QuaZip::open(): ZIP already opened"); + return false; + } + QIODevice *ioDevice = p->ioDevice; + if (ioDevice == NULL) { + if (p->zipName.isEmpty()) { + qWarning("QuaZip::open(): set either ZIP file name or IO device first"); + return false; + } else { + ioDevice = new QFile(p->zipName); + } + } + unsigned flags = 0; + switch(mode) { + case mdUnzip: + if (ioApi == NULL) { + if (p->autoClose) + flags |= UNZ_AUTO_CLOSE; + p->unzFile_f=unzOpenInternal(ioDevice, NULL, 1, flags); + } else { + // QuaZIP pre-zip64 compatibility mode + p->unzFile_f=unzOpen2(ioDevice, ioApi); + if (p->unzFile_f != NULL) { + if (p->autoClose) { + unzSetFlags(p->unzFile_f, UNZ_AUTO_CLOSE); + } else { + unzClearFlags(p->unzFile_f, UNZ_AUTO_CLOSE); + } + } + } + if(p->unzFile_f!=NULL) { + if (ioDevice->isSequential()) { + unzClose(p->unzFile_f); + if (!p->zipName.isEmpty()) + delete ioDevice; + qWarning("QuaZip::open(): " + "only mdCreate can be used with " + "sequential devices"); + return false; + } + p->mode=mode; + p->ioDevice = ioDevice; + return true; + } else { + p->zipError=UNZ_OPENERROR; + if (!p->zipName.isEmpty()) + delete ioDevice; + return false; + } + case mdCreate: + case mdAppend: + case mdAdd: + if (ioApi == NULL) { + if (p->autoClose) + flags |= ZIP_AUTO_CLOSE; + if (p->dataDescriptorWritingEnabled) + flags |= ZIP_WRITE_DATA_DESCRIPTOR; + p->zipFile_f=zipOpen3(ioDevice, + mode==mdCreate?APPEND_STATUS_CREATE: + mode==mdAppend?APPEND_STATUS_CREATEAFTER: + APPEND_STATUS_ADDINZIP, + NULL, NULL, flags); + } else { + // QuaZIP pre-zip64 compatibility mode + p->zipFile_f=zipOpen2(ioDevice, + mode==mdCreate?APPEND_STATUS_CREATE: + mode==mdAppend?APPEND_STATUS_CREATEAFTER: + APPEND_STATUS_ADDINZIP, + NULL, + ioApi); + if (p->zipFile_f != NULL) { + zipSetFlags(p->zipFile_f, flags); + } + } + if(p->zipFile_f!=NULL) { + if (ioDevice->isSequential()) { + if (mode != mdCreate) { + zipClose(p->zipFile_f, NULL); + qWarning("QuaZip::open(): " + "only mdCreate can be used with " + "sequential devices"); + if (!p->zipName.isEmpty()) + delete ioDevice; + return false; + } + zipSetFlags(p->zipFile_f, ZIP_SEQUENTIAL); + } + p->mode=mode; + p->ioDevice = ioDevice; + return true; + } else { + p->zipError=UNZ_OPENERROR; + if (!p->zipName.isEmpty()) + delete ioDevice; + return false; + } + default: + qWarning("QuaZip::open(): unknown mode: %d", (int)mode); + if (!p->zipName.isEmpty()) + delete ioDevice; + return false; + break; + } +} + +void QuaZip::close() +{ + p->zipError=UNZ_OK; + switch(p->mode) { + case mdNotOpen: + qWarning("QuaZip::close(): ZIP is not open"); + return; + case mdUnzip: + p->zipError=unzClose(p->unzFile_f); + break; + case mdCreate: + case mdAppend: + case mdAdd: + p->zipError=zipClose(p->zipFile_f, + p->comment.isNull() ? NULL + : p->commentCodec->fromUnicode(p->comment).constData()); + break; + default: + qWarning("QuaZip::close(): unknown mode: %d", (int)p->mode); + return; + } + // opened by name, need to delete the internal IO device + if (!p->zipName.isEmpty()) { + delete p->ioDevice; + p->ioDevice = NULL; + } + p->clearDirectoryMap(); + if(p->zipError==UNZ_OK) + p->mode=mdNotOpen; +} + +void QuaZip::setZipName(const QString& zipName) +{ + if(isOpen()) { + qWarning("QuaZip::setZipName(): ZIP is already open!"); + return; + } + p->zipName=zipName; + p->ioDevice = NULL; +} + +void QuaZip::setIoDevice(QIODevice *ioDevice) +{ + if(isOpen()) { + qWarning("QuaZip::setIoDevice(): ZIP is already open!"); + return; + } + p->ioDevice = ioDevice; + p->zipName = QString(); +} + +int QuaZip::getEntriesCount()const +{ + QuaZip *fakeThis=(QuaZip*)this; // non-const + fakeThis->p->zipError=UNZ_OK; + if(p->mode!=mdUnzip) { + qWarning("QuaZip::getEntriesCount(): ZIP is not open in mdUnzip mode"); + return -1; + } + unz_global_info64 globalInfo; + if((fakeThis->p->zipError=unzGetGlobalInfo64(p->unzFile_f, &globalInfo))!=UNZ_OK) + return p->zipError; + return (int)globalInfo.number_entry; +} + +QString QuaZip::getComment()const +{ + QuaZip *fakeThis=(QuaZip*)this; // non-const + fakeThis->p->zipError=UNZ_OK; + if(p->mode!=mdUnzip) { + qWarning("QuaZip::getComment(): ZIP is not open in mdUnzip mode"); + return QString(); + } + unz_global_info64 globalInfo; + QByteArray comment; + if((fakeThis->p->zipError=unzGetGlobalInfo64(p->unzFile_f, &globalInfo))!=UNZ_OK) + return QString(); + comment.resize(globalInfo.size_comment); + if((fakeThis->p->zipError=unzGetGlobalComment(p->unzFile_f, comment.data(), comment.size())) < 0) + return QString(); + fakeThis->p->zipError = UNZ_OK; + return p->commentCodec->toUnicode(comment); +} + +bool QuaZip::setCurrentFile(const QString& fileName, CaseSensitivity cs) +{ + p->zipError=UNZ_OK; + if(p->mode!=mdUnzip) { + qWarning("QuaZip::setCurrentFile(): ZIP is not open in mdUnzip mode"); + return false; + } + if(fileName.isEmpty()) { + p->hasCurrentFile_f=false; + return true; + } + // Unicode-aware reimplementation of the unzLocateFile function + if(p->unzFile_f==NULL) { + p->zipError=UNZ_PARAMERROR; + return false; + } + if(fileName.length()>MAX_FILE_NAME_LENGTH) { + p->zipError=UNZ_PARAMERROR; + return false; + } + // Find the file by name + bool sens = convertCaseSensitivity(cs) == Qt::CaseSensitive; + QString lower, current; + if(!sens) lower=fileName.toLower(); + p->hasCurrentFile_f=false; + + // Check the appropriate Map + unz64_file_pos fileDirPos; + fileDirPos.pos_in_zip_directory = 0; + if (sens) { + if (p->directoryCaseSensitive.contains(fileName)) + fileDirPos = p->directoryCaseSensitive.value(fileName); + } else { + if (p->directoryCaseInsensitive.contains(lower)) + fileDirPos = p->directoryCaseInsensitive.value(lower); + } + + if (fileDirPos.pos_in_zip_directory != 0) { + p->zipError = unzGoToFilePos64(p->unzFile_f, &fileDirPos); + p->hasCurrentFile_f = p->zipError == UNZ_OK; + } + + if (p->hasCurrentFile_f) + return p->hasCurrentFile_f; + + // Not mapped yet, start from where we have got to so far + for(bool more=p->goToFirstUnmappedFile(); more; more=goToNextFile()) { + current=getCurrentFileName(); + if(current.isEmpty()) return false; + if(sens) { + if(current==fileName) break; + } else { + if(current.toLower()==lower) break; + } + } + return p->hasCurrentFile_f; +} + +bool QuaZip::goToFirstFile() +{ + p->zipError=UNZ_OK; + if(p->mode!=mdUnzip) { + qWarning("QuaZip::goToFirstFile(): ZIP is not open in mdUnzip mode"); + return false; + } + p->zipError=unzGoToFirstFile(p->unzFile_f); + p->hasCurrentFile_f=p->zipError==UNZ_OK; + return p->hasCurrentFile_f; +} + +bool QuaZip::goToNextFile() +{ + p->zipError=UNZ_OK; + if(p->mode!=mdUnzip) { + qWarning("QuaZip::goToFirstFile(): ZIP is not open in mdUnzip mode"); + return false; + } + p->zipError=unzGoToNextFile(p->unzFile_f); + p->hasCurrentFile_f=p->zipError==UNZ_OK; + if(p->zipError==UNZ_END_OF_LIST_OF_FILE) + p->zipError=UNZ_OK; + return p->hasCurrentFile_f; +} + +bool QuaZip::getCurrentFileInfo(QuaZipFileInfo *info)const +{ + QuaZipFileInfo64 info64; + if (info == NULL) { // Very unlikely because of the overloads + return false; + } + if (getCurrentFileInfo(&info64)) { + info64.toQuaZipFileInfo(*info); + return true; + } else { + return false; + } +} + +bool QuaZip::getCurrentFileInfo(QuaZipFileInfo64 *info)const +{ + QuaZip *fakeThis=(QuaZip*)this; // non-const + fakeThis->p->zipError=UNZ_OK; + if(p->mode!=mdUnzip) { + qWarning("QuaZip::getCurrentFileInfo(): ZIP is not open in mdUnzip mode"); + return false; + } + unz_file_info64 info_z; + QByteArray fileName; + QByteArray extra; + QByteArray comment; + if(info==NULL) return false; + if(!isOpen()||!hasCurrentFile()) return false; + if((fakeThis->p->zipError=unzGetCurrentFileInfo64(p->unzFile_f, &info_z, NULL, 0, NULL, 0, NULL, 0))!=UNZ_OK) + return false; + fileName.resize(info_z.size_filename); + extra.resize(info_z.size_file_extra); + comment.resize(info_z.size_file_comment); + if((fakeThis->p->zipError=unzGetCurrentFileInfo64(p->unzFile_f, NULL, + fileName.data(), fileName.size(), + extra.data(), extra.size(), + comment.data(), comment.size()))!=UNZ_OK) + return false; + info->versionCreated=info_z.version; + info->versionNeeded=info_z.version_needed; + info->flags=info_z.flag; + info->method=info_z.compression_method; + info->crc=info_z.crc; + info->compressedSize=info_z.compressed_size; + info->uncompressedSize=info_z.uncompressed_size; + info->diskNumberStart=info_z.disk_num_start; + info->internalAttr=info_z.internal_fa; + info->externalAttr=info_z.external_fa; + info->name=p->fileNameCodec->toUnicode(fileName); + info->comment=p->commentCodec->toUnicode(comment); + info->extra=extra; + info->dateTime=QDateTime( + QDate(info_z.tmu_date.tm_year, info_z.tmu_date.tm_mon+1, info_z.tmu_date.tm_mday), + QTime(info_z.tmu_date.tm_hour, info_z.tmu_date.tm_min, info_z.tmu_date.tm_sec)); + // Add to directory map + p->addCurrentFileToDirectoryMap(info->name); + return true; +} + +QString QuaZip::getCurrentFileName()const +{ + QuaZip *fakeThis=(QuaZip*)this; // non-const + fakeThis->p->zipError=UNZ_OK; + if(p->mode!=mdUnzip) { + qWarning("QuaZip::getCurrentFileName(): ZIP is not open in mdUnzip mode"); + return QString(); + } + if(!isOpen()||!hasCurrentFile()) return QString(); + QByteArray fileName(MAX_FILE_NAME_LENGTH, 0); + if((fakeThis->p->zipError=unzGetCurrentFileInfo64(p->unzFile_f, NULL, fileName.data(), fileName.size(), + NULL, 0, NULL, 0))!=UNZ_OK) + return QString(); + QString result = p->fileNameCodec->toUnicode(fileName.constData()); + if (result.isEmpty()) + return result; + // Add to directory map + p->addCurrentFileToDirectoryMap(result); + return result; +} + +void QuaZip::setFileNameCodec(QTextCodec *fileNameCodec) +{ + p->fileNameCodec=fileNameCodec; +} + +void QuaZip::setFileNameCodec(const char *fileNameCodecName) +{ + p->fileNameCodec=QTextCodec::codecForName(fileNameCodecName); +} + +QTextCodec *QuaZip::getFileNameCodec()const +{ + return p->fileNameCodec; +} + +void QuaZip::setCommentCodec(QTextCodec *commentCodec) +{ + p->commentCodec=commentCodec; +} + +void QuaZip::setCommentCodec(const char *commentCodecName) +{ + p->commentCodec=QTextCodec::codecForName(commentCodecName); +} + +QTextCodec *QuaZip::getCommentCodec()const +{ + return p->commentCodec; +} + +QString QuaZip::getZipName() const +{ + return p->zipName; +} + +QIODevice *QuaZip::getIoDevice() const +{ + if (!p->zipName.isEmpty()) // opened by name, using an internal QIODevice + return NULL; + return p->ioDevice; +} + +QuaZip::Mode QuaZip::getMode()const +{ + return p->mode; +} + +bool QuaZip::isOpen()const +{ + return p->mode!=mdNotOpen; +} + +int QuaZip::getZipError() const +{ + return p->zipError; +} + +void QuaZip::setComment(const QString& comment) +{ + p->comment=comment; +} + +bool QuaZip::hasCurrentFile()const +{ + return p->hasCurrentFile_f; +} + +unzFile QuaZip::getUnzFile() +{ + return p->unzFile_f; +} + +zipFile QuaZip::getZipFile() +{ + return p->zipFile_f; +} + +void QuaZip::setDataDescriptorWritingEnabled(bool enabled) +{ + p->dataDescriptorWritingEnabled = enabled; +} + +bool QuaZip::isDataDescriptorWritingEnabled() const +{ + return p->dataDescriptorWritingEnabled; +} + +template +TFileInfo QuaZip_getFileInfo(QuaZip *zip, bool *ok); + +template<> +QuaZipFileInfo QuaZip_getFileInfo(QuaZip *zip, bool *ok) +{ + QuaZipFileInfo info; + *ok = zip->getCurrentFileInfo(&info); + return info; +} + +template<> +QuaZipFileInfo64 QuaZip_getFileInfo(QuaZip *zip, bool *ok) +{ + QuaZipFileInfo64 info; + *ok = zip->getCurrentFileInfo(&info); + return info; +} + +template<> +QString QuaZip_getFileInfo(QuaZip *zip, bool *ok) +{ + QString name = zip->getCurrentFileName(); + *ok = !name.isEmpty(); + return name; +} + +template +bool QuaZipPrivate::getFileInfoList(QList *result) const +{ + QuaZipPrivate *fakeThis=const_cast(this); + fakeThis->zipError=UNZ_OK; + if (mode!=QuaZip::mdUnzip) { + qWarning("QuaZip::getFileNameList/getFileInfoList(): " + "ZIP is not open in mdUnzip mode"); + return false; + } + QString currentFile; + if (q->hasCurrentFile()) { + currentFile = q->getCurrentFileName(); + } + if (q->goToFirstFile()) { + do { + bool ok; + result->append(QuaZip_getFileInfo(q, &ok)); + if (!ok) + return false; + } while (q->goToNextFile()); + } + if (zipError != UNZ_OK) + return false; + if (currentFile.isEmpty()) { + if (!q->goToFirstFile()) + return false; + } else { + if (!q->setCurrentFile(currentFile)) + return false; + } + return true; +} + +QStringList QuaZip::getFileNameList() const +{ + QStringList list; + if (p->getFileInfoList(&list)) + return list; + else + return QStringList(); +} + +QList QuaZip::getFileInfoList() const +{ + QList list; + if (p->getFileInfoList(&list)) + return list; + else + return QList(); +} + +QList QuaZip::getFileInfoList64() const +{ + QList list; + if (p->getFileInfoList(&list)) + return list; + else + return QList(); +} + +Qt::CaseSensitivity QuaZip::convertCaseSensitivity(QuaZip::CaseSensitivity cs) +{ + if (cs == csDefault) { +#ifdef Q_OS_WIN + return Qt::CaseInsensitive; +#else + return Qt::CaseSensitive; +#endif + } else { + return cs == csSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive; + } +} + +void QuaZip::setDefaultFileNameCodec(QTextCodec *codec) +{ + QuaZipPrivate::defaultFileNameCodec = codec; +} + +void QuaZip::setDefaultFileNameCodec(const char *codecName) +{ + setDefaultFileNameCodec(QTextCodec::codecForName(codecName)); +} + +void QuaZip::setZip64Enabled(bool zip64) +{ + p->zip64 = zip64; +} + +bool QuaZip::isZip64Enabled() const +{ + return p->zip64; +} + +bool QuaZip::isAutoClose() const +{ + return p->autoClose; +} + +void QuaZip::setAutoClose(bool autoClose) const +{ + p->autoClose = autoClose; +} diff --git a/ultimmc/libraries/quazip/quazip/quazip.h b/ultimmc/libraries/quazip/quazip/quazip.h new file mode 100644 index 0000000..ae2c8f4 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quazip.h @@ -0,0 +1,571 @@ +#ifndef QUA_ZIP_H +#define QUA_ZIP_H + +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant, see +quazip/(un)zip.h files for details, basically it's zlib license. + **/ + +#include +#include +#include + +#include "zip.h" +#include "unzip.h" + +#include "quazip_global.h" +#include "quazipfileinfo.h" + +// just in case it will be defined in the later versions of the ZIP/UNZIP +#ifndef UNZ_OPENERROR +// define additional error code +#define UNZ_OPENERROR -1000 +#endif + +class QuaZipPrivate; + +/// ZIP archive. +/** \class QuaZip quazip.h + * This class implements basic interface to the ZIP archive. It can be + * used to read table contents of the ZIP archive and retreiving + * information about the files inside it. + * + * You can also use this class to open files inside archive by passing + * pointer to the instance of this class to the constructor of the + * QuaZipFile class. But see QuaZipFile::QuaZipFile(QuaZip*, QObject*) + * for the possible pitfalls. + * + * This class is indended to provide interface to the ZIP subpackage of + * the ZIP/UNZIP package as well as to the UNZIP subpackage. But + * currently it supports only UNZIP. + * + * The use of this class is simple - just create instance using + * constructor, then set ZIP archive file name using setFile() function + * (if you did not passed the name to the constructor), then open() and + * then use different functions to work with it! Well, if you are + * paranoid, you may also wish to call close before destructing the + * instance, to check for errors on close. + * + * You may also use getUnzFile() and getZipFile() functions to get the + * ZIP archive handle and use it with ZIP/UNZIP package API directly. + * + * This class supports localized file names inside ZIP archive, but you + * have to set up proper codec with setCodec() function. By default, + * locale codec will be used, which is probably ok for UNIX systems, but + * will almost certainly fail with ZIP archives created in Windows. This + * is because Windows ZIP programs have strange habit of using DOS + * encoding for file names in ZIP archives. For example, ZIP archive + * with cyrillic names created in Windows will have file names in \c + * IBM866 encoding instead of \c WINDOWS-1251. I think that calling one + * function is not much trouble, but for true platform independency it + * would be nice to have some mechanism for file name encoding auto + * detection using locale information. Does anyone know a good way to do + * it? + **/ +class QUAZIP_EXPORT QuaZip { + friend class QuaZipPrivate; + public: + /// Useful constants. + enum Constants { + MAX_FILE_NAME_LENGTH=256 /**< Maximum file name length. Taken from + \c UNZ_MAXFILENAMEINZIP constant in + unzip.c. */ + }; + /// Open mode of the ZIP file. + enum Mode { + mdNotOpen, ///< ZIP file is not open. This is the initial mode. + mdUnzip, ///< ZIP file is open for reading files inside it. + mdCreate, ///< ZIP file was created with open() call. + mdAppend, /**< ZIP file was opened in append mode. This refers to + * \c APPEND_STATUS_CREATEAFTER mode in ZIP/UNZIP package + * and means that zip is appended to some existing file + * what is useful when that file contains + * self-extractor code. This is obviously \em not what + * you whant to use to add files to the existing ZIP + * archive. + **/ + mdAdd ///< ZIP file was opened for adding files in the archive. + }; + /// Case sensitivity for the file names. + /** This is what you specify when accessing files in the archive. + * Works perfectly fine with any characters thanks to Qt's great + * unicode support. This is different from ZIP/UNZIP API, where + * only US-ASCII characters was supported. + **/ + enum CaseSensitivity { + csDefault=0, ///< Default for platform. Case sensitive for UNIX, not for Windows. + csSensitive=1, ///< Case sensitive. + csInsensitive=2 ///< Case insensitive. + }; + /// Returns the actual case sensitivity for the specified QuaZIP one. + /** + \param cs The value to convert. + \returns If CaseSensitivity::csDefault, then returns the default + file name case sensitivity for the platform. Otherwise, just + returns the appropriate value from the Qt::CaseSensitivity enum. + */ + static Qt::CaseSensitivity convertCaseSensitivity( + CaseSensitivity cs); + private: + QuaZipPrivate *p; + // not (and will not be) implemented + QuaZip(const QuaZip& that); + // not (and will not be) implemented + QuaZip& operator=(const QuaZip& that); + public: + /// Constructs QuaZip object. + /** Call setName() before opening constructed object. */ + QuaZip(); + /// Constructs QuaZip object associated with ZIP file \a zipName. + QuaZip(const QString& zipName); + /// Constructs QuaZip object associated with ZIP file represented by \a ioDevice. + /** The IO device must be seekable, otherwise an error will occur when opening. */ + QuaZip(QIODevice *ioDevice); + /// Destroys QuaZip object. + /** Calls close() if necessary. */ + ~QuaZip(); + /// Opens ZIP file. + /** + * Argument \a mode specifies open mode of the ZIP archive. See Mode + * for details. Note that there is zipOpen2() function in the + * ZIP/UNZIP API which accepts \a globalcomment argument, but it + * does not use it anywhere, so this open() function does not have this + * argument. See setComment() if you need to set global comment. + * + * If the ZIP file is accessed via explicitly set QIODevice, then + * this device is opened in the necessary mode. If the device was + * already opened by some other means, then QuaZIP checks if the + * open mode is compatible to the mode needed for the requested operation. + * If necessary, seeking is performed to position the device properly. + * + * \return \c true if successful, \c false otherwise. + * + * \note ZIP/UNZIP API open calls do not return error code - they + * just return \c NULL indicating an error. But to make things + * easier, quazip.h header defines additional error code \c + * UNZ_ERROROPEN and getZipError() will return it if the open call + * of the ZIP/UNZIP API returns \c NULL. + * + * Argument \a ioApi specifies IO function set for ZIP/UNZIP + * package to use. See unzip.h, zip.h and ioapi.h for details. Note + * that IO API for QuaZip is different from the original package. + * The file path argument was changed to be of type \c voidpf, and + * QuaZip passes a QIODevice pointer there. This QIODevice is either + * set explicitly via setIoDevice() or the QuaZip(QIODevice*) + * constructor, or it is created internally when opening the archive + * by its file name. The default API (qioapi.cpp) just delegates + * everything to the QIODevice API. Not only this allows to use a + * QIODevice instead of file name, but also has a nice side effect + * of raising the file size limit from 2G to 4G (in non-zip64 archives). + * + * \note If the zip64 support is needed, the ioApi argument \em must be NULL + * because due to the backwards compatibility issues it can be used to + * provide a 32-bit API only. + * + * \note If the \ref QuaZip::setAutoClose() "no-auto-close" feature is used, + * then the \a ioApi argument \em should be NULL because the old API + * doesn't support the 'fake close' operation, causing slight memory leaks + * and other possible troubles (like closing the output device in case + * when an error occurs during opening). + * + * In short: just forget about the \a ioApi argument and you'll be + * fine. + **/ + bool open(Mode mode, zlib_filefunc_def *ioApi =NULL); + /// Closes ZIP file. + /** Call getZipError() to determine if the close was successful. + * + * If the file was opened by name, then the underlying QIODevice is closed + * and deleted. + * + * If the underlying QIODevice was set explicitly using setIoDevice() or + * the appropriate constructor, then it is closed if the auto-close flag + * is set (which it is by default). Call setAutoClose() to clear the + * auto-close flag if this behavior is undesirable. + * + * Since Qt 5.1, the QSaveFile was introduced. It breaks the QIODevice API + * by making close() private and crashing the application if it is called + * from the base class where it is public. It is an excellent example + * of poor design that illustrates why you should never ever break + * an is-a relationship between the base class and a subclass. QuaZIP + * works around this bug by checking if the QIODevice is an instance + * of QSaveFile, using qobject_cast<>, and if it is, calls + * QSaveFile::commit() instead of close(). It is a really ugly hack, + * but at least it makes your programs work instead of crashing. Note that + * if the auto-close flag is cleared, then this is a non-issue, and + * commit() isn't called. + */ + void close(); + /// Sets the codec used to encode/decode file names inside archive. + /** This is necessary to access files in the ZIP archive created + * under Windows with non-latin characters in file names. For + * example, file names with cyrillic letters will be in \c IBM866 + * encoding. + **/ + void setFileNameCodec(QTextCodec *fileNameCodec); + /// Sets the codec used to encode/decode file names inside archive. + /** \overload + * Equivalent to calling setFileNameCodec(QTextCodec::codecForName(codecName)); + **/ + void setFileNameCodec(const char *fileNameCodecName); + /// Returns the codec used to encode/decode comments inside archive. + QTextCodec* getFileNameCodec() const; + /// Sets the codec used to encode/decode comments inside archive. + /** This codec defaults to locale codec, which is probably ok. + **/ + void setCommentCodec(QTextCodec *commentCodec); + /// Sets the codec used to encode/decode comments inside archive. + /** \overload + * Equivalent to calling setCommentCodec(QTextCodec::codecForName(codecName)); + **/ + void setCommentCodec(const char *commentCodecName); + /// Returns the codec used to encode/decode comments inside archive. + QTextCodec* getCommentCodec() const; + /// Returns the name of the ZIP file. + /** Returns null string if no ZIP file name has been set, for + * example when the QuaZip instance is set up to use a QIODevice + * instead. + * \sa setZipName(), setIoDevice(), getIoDevice() + **/ + QString getZipName() const; + /// Sets the name of the ZIP file. + /** Does nothing if the ZIP file is open. + * + * Does not reset error code returned by getZipError(). + * \sa setIoDevice(), getIoDevice(), getZipName() + **/ + void setZipName(const QString& zipName); + /// Returns the device representing this ZIP file. + /** Returns null string if no device has been set explicitly, for + * example when opening a ZIP file by name. + * \sa setIoDevice(), getZipName(), setZipName() + **/ + QIODevice *getIoDevice() const; + /// Sets the device representing the ZIP file. + /** Does nothing if the ZIP file is open. + * + * Does not reset error code returned by getZipError(). + * \sa getIoDevice(), getZipName(), setZipName() + **/ + void setIoDevice(QIODevice *ioDevice); + /// Returns the mode in which ZIP file was opened. + Mode getMode() const; + /// Returns \c true if ZIP file is open, \c false otherwise. + bool isOpen() const; + /// Returns the error code of the last operation. + /** Returns \c UNZ_OK if the last operation was successful. + * + * Error code resets to \c UNZ_OK every time you call any function + * that accesses something inside ZIP archive, even if it is \c + * const (like getEntriesCount()). open() and close() calls reset + * error code too. See documentation for the specific functions for + * details on error detection. + **/ + int getZipError() const; + /// Returns number of the entries in the ZIP central directory. + /** Returns negative error code in the case of error. The same error + * code will be returned by subsequent getZipError() call. + **/ + int getEntriesCount() const; + /// Returns global comment in the ZIP file. + QString getComment() const; + /// Sets the global comment in the ZIP file. + /** The comment will be written to the archive on close operation. + * QuaZip makes a distinction between a null QByteArray() comment + * and an empty "" comment in the QuaZip::mdAdd mode. + * A null comment is the default and it means "don't change + * the comment". An empty comment removes the original comment. + * + * \sa open() + **/ + void setComment(const QString& comment); + /// Sets the current file to the first file in the archive. + /** Returns \c true on success, \c false otherwise. Call + * getZipError() to get the error code. + **/ + bool goToFirstFile(); + /// Sets the current file to the next file in the archive. + /** Returns \c true on success, \c false otherwise. Call + * getZipError() to determine if there was an error. + * + * Should be used only in QuaZip::mdUnzip mode. + * + * \note If the end of file was reached, getZipError() will return + * \c UNZ_OK instead of \c UNZ_END_OF_LIST_OF_FILE. This is to make + * things like this easier: + * \code + * for(bool more=zip.goToFirstFile(); more; more=zip.goToNextFile()) { + * // do something + * } + * if(zip.getZipError()==UNZ_OK) { + * // ok, there was no error + * } + * \endcode + **/ + bool goToNextFile(); + /// Sets current file by its name. + /** Returns \c true if successful, \c false otherwise. Argument \a + * cs specifies case sensitivity of the file name. Call + * getZipError() in the case of a failure to get error code. + * + * This is not a wrapper to unzLocateFile() function. That is + * because I had to implement locale-specific case-insensitive + * comparison. + * + * Here are the differences from the original implementation: + * + * - If the file was not found, error code is \c UNZ_OK, not \c + * UNZ_END_OF_LIST_OF_FILE (see also goToNextFile()). + * - If this function fails, it unsets the current file rather than + * resetting it back to what it was before the call. + * + * If \a fileName is null string then this function unsets the + * current file and return \c true. Note that you should close the + * file first if it is open! See + * QuaZipFile::QuaZipFile(QuaZip*,QObject*) for the details. + * + * Should be used only in QuaZip::mdUnzip mode. + * + * \sa setFileNameCodec(), CaseSensitivity + **/ + bool setCurrentFile(const QString& fileName, CaseSensitivity cs =csDefault); + /// Returns \c true if the current file has been set. + bool hasCurrentFile() const; + /// Retrieves information about the current file. + /** Fills the structure pointed by \a info. Returns \c true on + * success, \c false otherwise. In the latter case structure pointed + * by \a info remains untouched. If there was an error, + * getZipError() returns error code. + * + * Should be used only in QuaZip::mdUnzip mode. + * + * Does nothing and returns \c false in any of the following cases. + * - ZIP is not open; + * - ZIP does not have current file. + * + * In both cases getZipError() returns \c UNZ_OK since there + * is no ZIP/UNZIP API call. + * + * This overload doesn't support zip64, but will work OK on zip64 archives + * except that if one of the sizes (compressed or uncompressed) is greater + * than 0xFFFFFFFFu, it will be set to exactly 0xFFFFFFFFu. + * + * \sa getCurrentFileInfo(QuaZipFileInfo64* info)const + * \sa QuaZipFileInfo64::toQuaZipFileInfo(QuaZipFileInfo&)const + **/ + bool getCurrentFileInfo(QuaZipFileInfo* info)const; + /// Retrieves information about the current file. + /** \overload + * + * This function supports zip64. If the archive doesn't use zip64, it is + * completely equivalent to getCurrentFileInfo(QuaZipFileInfo* info) + * except for the argument type. + * + * \sa + **/ + bool getCurrentFileInfo(QuaZipFileInfo64* info)const; + /// Returns the current file name. + /** Equivalent to calling getCurrentFileInfo() and then getting \c + * name field of the QuaZipFileInfo structure, but faster and more + * convenient. + * + * Should be used only in QuaZip::mdUnzip mode. + **/ + QString getCurrentFileName()const; + /// Returns \c unzFile handle. + /** You can use this handle to directly call UNZIP part of the + * ZIP/UNZIP package functions (see unzip.h). + * + * \warning When using the handle returned by this function, please + * keep in mind that QuaZip class is unable to detect any changes + * you make in the ZIP file state (e. g. changing current file, or + * closing the handle). So please do not do anything with this + * handle that is possible to do with the functions of this class. + * Or at least return the handle in the original state before + * calling some another function of this class (including implicit + * destructor calls and calls from the QuaZipFile objects that refer + * to this QuaZip instance!). So if you have changed the current + * file in the ZIP archive - then change it back or you may + * experience some strange behavior or even crashes. + **/ + unzFile getUnzFile(); + /// Returns \c zipFile handle. + /** You can use this handle to directly call ZIP part of the + * ZIP/UNZIP package functions (see zip.h). Warnings about the + * getUnzFile() function also apply to this function. + **/ + zipFile getZipFile(); + /// Changes the data descriptor writing mode. + /** + According to the ZIP format specification, a file inside archive + may have a data descriptor immediately following the file + data. This is reflected by a special flag in the local file header + and in the central directory. By default, QuaZIP sets this flag + and writes the data descriptor unless both method and level were + set to 0, in which case it operates in 1.0-compatible mode and + never writes data descriptors. + + By setting this flag to false, it is possible to disable data + descriptor writing, thus increasing compatibility with archive + readers that don't understand this feature of the ZIP file format. + + Setting this flag affects all the QuaZipFile instances that are + opened after this flag is set. + + The data descriptor writing mode is enabled by default. + + Note that if the ZIP archive is written into a QIODevice for which + QIODevice::isSequential() returns \c true, then the data descriptor + is mandatory and will be written even if this flag is set to false. + + \param enabled If \c true, enable local descriptor writing, + disable it otherwise. + + \sa QuaZipFile::isDataDescriptorWritingEnabled() + */ + void setDataDescriptorWritingEnabled(bool enabled); + /// Returns the data descriptor default writing mode. + /** + \sa setDataDescriptorWritingEnabled() + */ + bool isDataDescriptorWritingEnabled() const; + /// Returns a list of files inside the archive. + /** + \return A list of file names or an empty list if there + was an error or if the archive is empty (call getZipError() to + figure out which). + \sa getFileInfoList() + */ + QStringList getFileNameList() const; + /// Returns information list about all files inside the archive. + /** + \return A list of QuaZipFileInfo objects or an empty list if there + was an error or if the archive is empty (call getZipError() to + figure out which). + + This function doesn't support zip64, but will still work with zip64 + archives, converting results using QuaZipFileInfo64::toQuaZipFileInfo(). + If all file sizes are below 4 GB, it will work just fine. + + \sa getFileNameList() + \sa getFileInfoList64() + */ + QList getFileInfoList() const; + /// Returns information list about all files inside the archive. + /** + \overload + + This function supports zip64. + + \sa getFileNameList() + \sa getFileInfoList() + */ + QList getFileInfoList64() const; + /// Enables the zip64 mode. + /** + * @param zip64 If \c true, the zip64 mode is enabled, disabled otherwise. + * + * Once this is enabled, all new files (until the mode is disabled again) + * will be created in the zip64 mode, thus enabling the ability to write + * files larger than 4 GB. By default, the zip64 mode is off due to + * compatibility reasons. + * + * Note that this does not affect the ability to read zip64 archives in any + * way. + * + * \sa isZip64Enabled() + */ + void setZip64Enabled(bool zip64); + /// Returns whether the zip64 mode is enabled. + /** + * @return \c true if and only if the zip64 mode is enabled. + * + * \sa setZip64Enabled() + */ + bool isZip64Enabled() const; + /// Returns the auto-close flag. + /** + @sa setAutoClose() + */ + bool isAutoClose() const; + /// Sets or unsets the auto-close flag. + /** + By default, QuaZIP opens the underlying QIODevice when open() is called, + and closes it when close() is called. In some cases, when the device + is set explicitly using setIoDevice(), it may be desirable to + leave the device open. If the auto-close flag is unset using this method, + then the device isn't closed automatically if it was set explicitly. + + If it is needed to clear this flag, it is recommended to do so before + opening the archive because otherwise QuaZIP may close the device + during the open() call if an error is encountered after the device + is opened. + + If the device was not set explicitly, but rather the setZipName() or + the appropriate constructor was used to set the ZIP file name instead, + then the auto-close flag has no effect, and the internal device + is closed nevertheless because there is no other way to close it. + + @sa isAutoClose() + @sa setIoDevice() + */ + void setAutoClose(bool autoClose) const; + /// Sets the default file name codec to use. + /** + * The default codec is used by the constructors, so calling this function + * won't affect the QuaZip instances already created at that moment. + * + * The codec specified here can be overriden by calling setFileNameCodec(). + * If neither function is called, QTextCodec::codecForLocale() will be used + * to decode or encode file names. Use this function with caution if + * the application uses other libraries that depend on QuaZIP. Those + * libraries can either call this function by themselves, thus overriding + * your setting or can rely on the default encoding, thus failing + * mysteriously if you change it. For these reasons, it isn't recommended + * to use this function if you are developing a library, not an application. + * Instead, ask your library users to call it in case they need specific + * encoding. + * + * In most cases, using setFileNameCodec() instead is the right choice. + * However, if you depend on third-party code that uses QuaZIP, then the + * reasons stated above can actually become a reason to use this function + * in case the third-party code in question fails because it doesn't + * understand the encoding you need and doesn't provide a way to specify it. + * This applies to the JlCompress class as well, as it was contributed and + * doesn't support explicit encoding parameters. + * + * In short: use setFileNameCodec() when you can, resort to + * setDefaultFileNameCodec() when you don't have access to the QuaZip + * instance. + * + * @param codec The codec to use by default. If NULL, resets to default. + */ + static void setDefaultFileNameCodec(QTextCodec *codec); + /** + * @overload + * Equivalent to calling + * setDefltFileNameCodec(QTextCodec::codecForName(codecName)). + */ + static void setDefaultFileNameCodec(const char *codecName); +}; + +#endif diff --git a/ultimmc/libraries/quazip/quazip/quazip_global.h b/ultimmc/libraries/quazip/quazip/quazip_global.h new file mode 100644 index 0000000..7e3798a --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quazip_global.h @@ -0,0 +1,59 @@ +#ifndef QUAZIP_GLOBAL_H +#define QUAZIP_GLOBAL_H + +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include + +/** + This is automatically defined when building a static library, but when + including QuaZip sources directly into a project, QUAZIP_STATIC should + be defined explicitly to avoid possible troubles with unnecessary + importing/exporting. + */ +#ifdef QUAZIP_STATIC +#define QUAZIP_EXPORT +#else +/** + * When building a DLL with MSVC, QUAZIP_BUILD must be defined. + * qglobal.h takes care of defining Q_DECL_* correctly for msvc/gcc. + */ +#if defined(QUAZIP_BUILD) + #define QUAZIP_EXPORT Q_DECL_EXPORT +#else + #define QUAZIP_EXPORT Q_DECL_IMPORT +#endif +#endif // QUAZIP_STATIC + +#ifdef __GNUC__ +#define UNUSED __attribute__((__unused__)) +#else +#define UNUSED +#endif + +#define QUAZIP_EXTRA_NTFS_MAGIC 0x000Au +#define QUAZIP_EXTRA_NTFS_TIME_MAGIC 0x0001u + +#endif // QUAZIP_GLOBAL_H diff --git a/ultimmc/libraries/quazip/quazip/quazipdir.cpp b/ultimmc/libraries/quazip/quazip/quazipdir.cpp new file mode 100644 index 0000000..5ae08a0 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quazipdir.cpp @@ -0,0 +1,567 @@ +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include "quazipdir.h" + +#include +#include + +/// \cond internal +class QuaZipDirPrivate: public QSharedData { + friend class QuaZipDir; +private: + QuaZipDirPrivate(QuaZip *zip, const QString &dir = QString()): + zip(zip), dir(dir), caseSensitivity(QuaZip::csDefault), + filter(QDir::NoFilter), sorting(QDir::NoSort) {} + QuaZip *zip; + QString dir; + QuaZip::CaseSensitivity caseSensitivity; + QDir::Filters filter; + QStringList nameFilters; + QDir::SortFlags sorting; + template + bool entryInfoList(QStringList nameFilters, QDir::Filters filter, + QDir::SortFlags sort, TFileInfoList &result) const; + inline QString simplePath() const {return QDir::cleanPath(dir);} +}; +/// \endcond + +QuaZipDir::QuaZipDir(const QuaZipDir &that): + d(that.d) +{ +} + +QuaZipDir::QuaZipDir(QuaZip *zip, const QString &dir): + d(new QuaZipDirPrivate(zip, dir)) +{ + if (d->dir.startsWith('/')) + d->dir = d->dir.mid(1); +} + +QuaZipDir::~QuaZipDir() +{ +} + +bool QuaZipDir::operator==(const QuaZipDir &that) +{ + return d->zip == that.d->zip && d->dir == that.d->dir; +} + +QuaZipDir& QuaZipDir::operator=(const QuaZipDir &that) +{ + this->d = that.d; + return *this; +} + +QString QuaZipDir::operator[](int pos) const +{ + return entryList().at(pos); +} + +QuaZip::CaseSensitivity QuaZipDir::caseSensitivity() const +{ + return d->caseSensitivity; +} + +bool QuaZipDir::cd(const QString &directoryName) +{ + if (directoryName == "/") { + d->dir = ""; + return true; + } + QString dirName = directoryName; + if (dirName.endsWith('/')) + dirName.chop(1); + if (dirName.contains('/')) { + QuaZipDir dir(*this); + if (dirName.startsWith('/')) { +#ifdef QUAZIP_QUAZIPDIR_DEBUG + qDebug("QuaZipDir::cd(%s): going to /", + dirName.toUtf8().constData()); +#endif + if (!dir.cd("/")) + return false; + } + QStringList path = dirName.split('/', QString::SkipEmptyParts); + for (QStringList::const_iterator i = path.constBegin(); + i != path.end(); + ++i) { + const QString &step = *i; +#ifdef QUAZIP_QUAZIPDIR_DEBUG + qDebug("QuaZipDir::cd(%s): going to %s", + dirName.toUtf8().constData(), + step.toUtf8().constData()); +#endif + if (!dir.cd(step)) + return false; + } + d->dir = dir.path(); + return true; + } else { // no '/' + if (dirName == ".") { + return true; + } else if (dirName == "..") { + if (isRoot()) { + return false; + } else { + int slashPos = d->dir.lastIndexOf('/'); + if (slashPos == -1) { + d->dir = ""; + } else { + d->dir = d->dir.left(slashPos); + } + return true; + } + } else { // a simple subdirectory + if (exists(dirName)) { + if (isRoot()) + d->dir = dirName; + else + d->dir += "/" + dirName; + return true; + } else { + return false; + } + } + } +} + +bool QuaZipDir::cdUp() +{ + return cd(".."); +} + +uint QuaZipDir::count() const +{ + return entryList().count(); +} + +QString QuaZipDir::dirName() const +{ + return QDir(d->dir).dirName(); +} + +QuaZipFileInfo64 QuaZipDir_getFileInfo(QuaZip *zip, bool *ok, + const QString &relativeName, + bool isReal) +{ + QuaZipFileInfo64 info; + if (isReal) { + *ok = zip->getCurrentFileInfo(&info); + } else { + *ok = true; + info.compressedSize = 0; + info.crc = 0; + info.diskNumberStart = 0; + info.externalAttr = 0; + info.flags = 0; + info.internalAttr = 0; + info.method = 0; + info.uncompressedSize = 0; + info.versionCreated = info.versionNeeded = 0; + } + info.name = relativeName; + return info; +} + +static void QuaZipDir_convertInfoList(const QList &from, + QList &to) +{ + to = from; +} + +static void QuaZipDir_convertInfoList(const QList &from, + QStringList &to) +{ + to.clear(); + for (QList::const_iterator i = from.constBegin(); + i != from.constEnd(); + ++i) { + to.append(i->name); + } +} + +static void QuaZipDir_convertInfoList(const QList &from, + QList &to) +{ + to.clear(); + for (QList::const_iterator i = from.constBegin(); + i != from.constEnd(); + ++i) { + QuaZipFileInfo info32; + i->toQuaZipFileInfo(info32); + to.append(info32); + } +} + +/// \cond internal +/** + An utility class to restore the current file. + */ +class QuaZipDirRestoreCurrent { +public: + inline QuaZipDirRestoreCurrent(QuaZip *zip): + zip(zip), currentFile(zip->getCurrentFileName()) {} + inline ~QuaZipDirRestoreCurrent() + { + zip->setCurrentFile(currentFile); + } +private: + QuaZip *zip; + QString currentFile; +}; +/// \endcond + +/// \cond internal +class QuaZipDirComparator +{ + private: + QDir::SortFlags sort; + static QString getExtension(const QString &name); + int compareStrings(const QString &string1, const QString &string2); + public: + inline QuaZipDirComparator(QDir::SortFlags sort): sort(sort) {} + bool operator()(const QuaZipFileInfo64 &info1, const QuaZipFileInfo64 &info2); +}; + +QString QuaZipDirComparator::getExtension(const QString &name) +{ + if (name.endsWith('.') || name.indexOf('.', 1) == -1) { + return ""; + } else { + return name.mid(name.lastIndexOf('.') + 1); + } + +} + +int QuaZipDirComparator::compareStrings(const QString &string1, + const QString &string2) +{ + if (sort & QDir::LocaleAware) { + if (sort & QDir::IgnoreCase) { + return string1.toLower().localeAwareCompare(string2.toLower()); + } else { + return string1.localeAwareCompare(string2); + } + } else { + return string1.compare(string2, (sort & QDir::IgnoreCase) + ? Qt::CaseInsensitive : Qt::CaseSensitive); + } +} + +bool QuaZipDirComparator::operator()(const QuaZipFileInfo64 &info1, + const QuaZipFileInfo64 &info2) +{ + QDir::SortFlags order = sort + & (QDir::Name | QDir::Time | QDir::Size | QDir::Type); + if ((sort & QDir::DirsFirst) == QDir::DirsFirst + || (sort & QDir::DirsLast) == QDir::DirsLast) { + if (info1.name.endsWith('/') && !info2.name.endsWith('/')) + return (sort & QDir::DirsFirst) == QDir::DirsFirst; + else if (!info1.name.endsWith('/') && info2.name.endsWith('/')) + return (sort & QDir::DirsLast) == QDir::DirsLast; + } + bool result; + int extDiff; + switch (order) { + case QDir::Name: + result = compareStrings(info1.name, info2.name) < 0; + break; + case QDir::Type: + extDiff = compareStrings(getExtension(info1.name), + getExtension(info2.name)); + if (extDiff == 0) { + result = compareStrings(info1.name, info2.name) < 0; + } else { + result = extDiff < 0; + } + break; + case QDir::Size: + if (info1.uncompressedSize == info2.uncompressedSize) { + result = compareStrings(info1.name, info2.name) < 0; + } else { + result = info1.uncompressedSize < info2.uncompressedSize; + } + break; + case QDir::Time: + if (info1.dateTime == info2.dateTime) { + result = compareStrings(info1.name, info2.name) < 0; + } else { + result = info1.dateTime < info2.dateTime; + } + break; + default: + qWarning("QuaZipDirComparator(): Invalid sort mode 0x%2X", + static_cast(sort)); + return false; + } + return (sort & QDir::Reversed) ? !result : result; +} + +template +bool QuaZipDirPrivate::entryInfoList(QStringList nameFilters, + QDir::Filters filter, QDir::SortFlags sort, TFileInfoList &result) const +{ + QString basePath = simplePath(); + if (!basePath.isEmpty()) + basePath += "/"; + int baseLength = basePath.length(); + result.clear(); + QuaZipDirRestoreCurrent saveCurrent(zip); + if (!zip->goToFirstFile()) { + return zip->getZipError() == UNZ_OK; + } + QDir::Filters fltr = filter; + if (fltr == QDir::NoFilter) + fltr = this->filter; + if (fltr == QDir::NoFilter) + fltr = QDir::AllEntries; + QStringList nmfltr = nameFilters; + if (nmfltr.isEmpty()) + nmfltr = this->nameFilters; + QSet dirsFound; + QList list; + do { + QString name = zip->getCurrentFileName(); + if (!name.startsWith(basePath)) + continue; + QString relativeName = name.mid(baseLength); + if (relativeName.isEmpty()) + continue; + bool isDir = false; + bool isReal = true; + if (relativeName.contains('/')) { + int indexOfSlash = relativeName.indexOf('/'); + // something like "subdir/" + isReal = indexOfSlash == relativeName.length() - 1; + relativeName = relativeName.left(indexOfSlash + 1); + if (dirsFound.contains(relativeName)) + continue; + isDir = true; + } + dirsFound.insert(relativeName); + if ((fltr & QDir::Dirs) == 0 && isDir) + continue; + if ((fltr & QDir::Files) == 0 && !isDir) + continue; + if (!nmfltr.isEmpty() && !QDir::match(nmfltr, relativeName)) + continue; + bool ok; + QuaZipFileInfo64 info = QuaZipDir_getFileInfo(zip, &ok, relativeName, + isReal); + if (!ok) { + return false; + } + list.append(info); + } while (zip->goToNextFile()); + QDir::SortFlags srt = sort; + if (srt == QDir::NoSort) + srt = sorting; +#ifdef QUAZIP_QUAZIPDIR_DEBUG + qDebug("QuaZipDirPrivate::entryInfoList(): before sort:"); + foreach (QuaZipFileInfo64 info, list) { + qDebug("%s\t%s", info.name.toUtf8().constData(), + info.dateTime.toString(Qt::ISODate).toUtf8().constData()); + } +#endif + if (srt != QDir::NoSort && (srt & QDir::Unsorted) != QDir::Unsorted) { + if (QuaZip::convertCaseSensitivity(caseSensitivity) + == Qt::CaseInsensitive) + srt |= QDir::IgnoreCase; + QuaZipDirComparator lessThan(srt); + qSort(list.begin(), list.end(), lessThan); + } + QuaZipDir_convertInfoList(list, result); + return true; +} + +/// \endcond + +QList QuaZipDir::entryInfoList(const QStringList &nameFilters, + QDir::Filters filters, QDir::SortFlags sort) const +{ + QList result; + if (d->entryInfoList(nameFilters, filters, sort, result)) + return result; + else + return QList(); +} + +QList QuaZipDir::entryInfoList(QDir::Filters filters, + QDir::SortFlags sort) const +{ + return entryInfoList(QStringList(), filters, sort); +} + +QList QuaZipDir::entryInfoList64(const QStringList &nameFilters, + QDir::Filters filters, QDir::SortFlags sort) const +{ + QList result; + if (d->entryInfoList(nameFilters, filters, sort, result)) + return result; + else + return QList(); +} + +QList QuaZipDir::entryInfoList64(QDir::Filters filters, + QDir::SortFlags sort) const +{ + return entryInfoList64(QStringList(), filters, sort); +} + +QStringList QuaZipDir::entryList(const QStringList &nameFilters, + QDir::Filters filters, QDir::SortFlags sort) const +{ + QStringList result; + if (d->entryInfoList(nameFilters, filters, sort, result)) + return result; + else + return QStringList(); +} + +QStringList QuaZipDir::entryList(QDir::Filters filters, + QDir::SortFlags sort) const +{ + return entryList(QStringList(), filters, sort); +} + +bool QuaZipDir::exists(const QString &filePath) const +{ + if (filePath == "/" || filePath.isEmpty()) + return true; + QString fileName = filePath; + if (fileName.endsWith('/')) + fileName.chop(1); + if (fileName.contains('/')) { + QFileInfo fileInfo(fileName); +#ifdef QUAZIP_QUAZIPDIR_DEBUG + qDebug("QuaZipDir::exists(): fileName=%s, fileInfo.fileName()=%s, " + "fileInfo.path()=%s", fileName.toUtf8().constData(), + fileInfo.fileName().toUtf8().constData(), + fileInfo.path().toUtf8().constData()); +#endif + QuaZipDir dir(*this); + return dir.cd(fileInfo.path()) && dir.exists(fileInfo.fileName()); + } else { + if (fileName == "..") { + return !isRoot(); + } else if (fileName == ".") { + return true; + } else { + QStringList entries = entryList(QDir::AllEntries, QDir::NoSort); +#ifdef QUAZIP_QUAZIPDIR_DEBUG + qDebug("QuaZipDir::exists(): looking for %s", + fileName.toUtf8().constData()); + for (QStringList::const_iterator i = entries.constBegin(); + i != entries.constEnd(); + ++i) { + qDebug("QuaZipDir::exists(): entry: %s", + i->toUtf8().constData()); + } +#endif + Qt::CaseSensitivity cs = QuaZip::convertCaseSensitivity( + d->caseSensitivity); + if (filePath.endsWith('/')) { + return entries.contains(filePath, cs); + } else { + return entries.contains(fileName, cs) + || entries.contains(fileName + "/", cs); + } + } + } +} + +bool QuaZipDir::exists() const +{ + return QuaZipDir(d->zip).exists(d->dir); +} + +QString QuaZipDir::filePath(const QString &fileName) const +{ + return QDir(d->dir).filePath(fileName); +} + +QDir::Filters QuaZipDir::filter() +{ + return d->filter; +} + +bool QuaZipDir::isRoot() const +{ + return d->simplePath().isEmpty(); +} + +QStringList QuaZipDir::nameFilters() const +{ + return d->nameFilters; +} + +QString QuaZipDir::path() const +{ + return d->dir; +} + +QString QuaZipDir::relativeFilePath(const QString &fileName) const +{ + return QDir("/" + d->dir).relativeFilePath(fileName); +} + +void QuaZipDir::setCaseSensitivity(QuaZip::CaseSensitivity caseSensitivity) +{ + d->caseSensitivity = caseSensitivity; +} + +void QuaZipDir::setFilter(QDir::Filters filters) +{ + d->filter = filters; +} + +void QuaZipDir::setNameFilters(const QStringList &nameFilters) +{ + d->nameFilters = nameFilters; +} + +void QuaZipDir::setPath(const QString &path) +{ + QString newDir = path; + if (newDir == "/") { + d->dir = ""; + } else { + if (newDir.endsWith('/')) + newDir.chop(1); + if (newDir.startsWith('/')) + newDir = newDir.mid(1); + d->dir = newDir; + } +} + +void QuaZipDir::setSorting(QDir::SortFlags sort) +{ + d->sorting = sort; +} + +QDir::SortFlags QuaZipDir::sorting() const +{ + return d->sorting; +} diff --git a/ultimmc/libraries/quazip/quazip/quazipdir.h b/ultimmc/libraries/quazip/quazip/quazipdir.h new file mode 100644 index 0000000..4626b17 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quazipdir.h @@ -0,0 +1,223 @@ +#ifndef QUAZIP_QUAZIPDIR_H +#define QUAZIP_QUAZIPDIR_H + +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +class QuaZipDirPrivate; + +#include "quazip.h" +#include "quazipfileinfo.h" +#include +#include +#include + +/// Provides ZIP archive navigation. +/** +* This class is modelled after QDir, and is designed to provide similar +* features for ZIP archives. +* +* The only significant difference from QDir is that the root path is not +* '/', but an empty string since that's how the file paths are stored in +* the archive. However, QuaZipDir understands the paths starting with +* '/'. It is important in a few places: +* +* - In the cd() function. +* - In the constructor. +* - In the exists() function. +* - In the relativePath() function. +* +* Note that since ZIP uses '/' on all platforms, the '\' separator is +* not supported. +*/ +class QUAZIP_EXPORT QuaZipDir { +private: + QSharedDataPointer d; +public: + /// The copy constructor. + QuaZipDir(const QuaZipDir &that); + /// Constructs a QuaZipDir instance pointing to the specified directory. + /** + If \a dir is not specified, points to the root of the archive. + The same happens if the \a dir is "/". + */ + QuaZipDir(QuaZip *zip, const QString &dir = QString()); + /// Destructor. + ~QuaZipDir(); + /// The assignment operator. + bool operator==(const QuaZipDir &that); + /// operator!= + /** + \return \c true if either this and \a that use different QuaZip + instances or if they point to different directories. + */ + inline bool operator!=(const QuaZipDir &that) {return !operator==(that);} + /// operator== + /** + \return \c true if both this and \a that use the same QuaZip + instance and point to the same directory. + */ + QuaZipDir& operator=(const QuaZipDir &that); + /// Returns the name of the entry at the specified position. + QString operator[](int pos) const; + /// Returns the current case sensitivity mode. + QuaZip::CaseSensitivity caseSensitivity() const; + /// Changes the 'current' directory. + /** + * If the path starts with '/', it is interpreted as an absolute + * path from the root of the archive. Otherwise, it is interpreted + * as a path relative to the current directory as was set by the + * previous cd() or the constructor. + * + * Note that the subsequent path() call will not return a path + * starting with '/' in all cases. + */ + bool cd(const QString &dirName); + /// Goes up. + bool cdUp(); + /// Returns the number of entries in the directory. + uint count() const; + /// Returns the current directory name. + /** + The name doesn't include the path. + */ + QString dirName() const; + /// Returns the list of the entries in the directory. + /** + \param nameFilters The list of file patterns to list, uses the same + syntax as QDir. + \param filters The entry type filters, only Files and Dirs are + accepted. + \param sort Sorting mode. + */ + QList entryInfoList(const QStringList &nameFilters, + QDir::Filters filters = QDir::NoFilter, + QDir::SortFlags sort = QDir::NoSort) const; + /// Returns the list of the entries in the directory. + /** + \overload + + The same as entryInfoList(QStringList(), filters, sort). + */ + QList entryInfoList(QDir::Filters filters = QDir::NoFilter, + QDir::SortFlags sort = QDir::NoSort) const; + /// Returns the list of the entries in the directory with zip64 support. + /** + \param nameFilters The list of file patterns to list, uses the same + syntax as QDir. + \param filters The entry type filters, only Files and Dirs are + accepted. + \param sort Sorting mode. + */ + QList entryInfoList64(const QStringList &nameFilters, + QDir::Filters filters = QDir::NoFilter, + QDir::SortFlags sort = QDir::NoSort) const; + /// Returns the list of the entries in the directory with zip64 support. + /** + \overload + + The same as entryInfoList64(QStringList(), filters, sort). + */ + QList entryInfoList64(QDir::Filters filters = QDir::NoFilter, + QDir::SortFlags sort = QDir::NoSort) const; + /// Returns the list of the entry names in the directory. + /** + The same as entryInfoList(nameFilters, filters, sort), but only + returns entry names. + */ + QStringList entryList(const QStringList &nameFilters, + QDir::Filters filters = QDir::NoFilter, + QDir::SortFlags sort = QDir::NoSort) const; + /// Returns the list of the entry names in the directory. + /** + \overload + + The same as entryList(QStringList(), filters, sort). + */ + QStringList entryList(QDir::Filters filters = QDir::NoFilter, + QDir::SortFlags sort = QDir::NoSort) const; + /// Returns \c true if the entry with the specified name exists. + /** + The ".." is considered to exist if the current directory + is not root. The "." and "/" are considered to + always exist. Paths starting with "/" are relative to + the archive root, other paths are relative to the current dir. + */ + bool exists(const QString &fileName) const; + /// Return \c true if the directory pointed by this QuaZipDir exists. + bool exists() const; + /// Returns the full path to the specified file. + /** + Doesn't check if the file actually exists. + */ + QString filePath(const QString &fileName) const; + /// Returns the default filter. + QDir::Filters filter(); + /// Returns if the QuaZipDir points to the root of the archive. + /** + Not that the root path is the empty string, not '/'. + */ + bool isRoot() const; + /// Return the default name filter. + QStringList nameFilters() const; + /// Returns the path to the current dir. + /** + The path never starts with '/', and the root path is an empty + string. + */ + QString path() const; + /// Returns the path to the specified file relative to the current dir. + /** + * This function is mostly useless, provided only for the sake of + * completeness. + * + * @param fileName The path to the file, should start with "/" + * if relative to the archive root. + * @return Path relative to the current dir. + */ + QString relativeFilePath(const QString &fileName) const; + /// Sets the default case sensitivity mode. + void setCaseSensitivity(QuaZip::CaseSensitivity caseSensitivity); + /// Sets the default filter. + void setFilter(QDir::Filters filters); + /// Sets the default name filter. + void setNameFilters(const QStringList &nameFilters); + /// Goes to the specified path. + /** + The difference from cd() is that this function never checks if the + path actually exists and doesn't use relative paths, so it's + possible to go to the root directory with setPath(""). + + Note that this function still chops the trailing and/or leading + '/' and treats a single '/' as the root path (path() will still + return an empty string). + */ + void setPath(const QString &path); + /// Sets the default sorting mode. + void setSorting(QDir::SortFlags sort); + /// Returns the default sorting mode. + QDir::SortFlags sorting() const; +}; + +#endif // QUAZIP_QUAZIPDIR_H diff --git a/ultimmc/libraries/quazip/quazip/quazipfile.cpp b/ultimmc/libraries/quazip/quazip/quazipfile.cpp new file mode 100644 index 0000000..3ce895a --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quazipfile.cpp @@ -0,0 +1,531 @@ +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant, see +quazip/(un)zip.h files for details, basically it's zlib license. + **/ + +#include "quazipfile.h" + +using namespace std; + +/// The implementation class for QuaZip. +/** +\internal + +This class contains all the private stuff for the QuaZipFile class, thus +allowing to preserve binary compatibility between releases, the +technique known as the Pimpl (private implementation) idiom. +*/ +class QuaZipFilePrivate { + friend class QuaZipFile; + private: + Q_DISABLE_COPY(QuaZipFilePrivate) + /// The pointer to the associated QuaZipFile instance. + QuaZipFile *q; + /// The QuaZip object to work with. + QuaZip *zip; + /// The file name. + QString fileName; + /// Case sensitivity mode. + QuaZip::CaseSensitivity caseSensitivity; + /// Whether this file is opened in the raw mode. + bool raw; + /// Write position to keep track of. + /** + QIODevice::pos() is broken for non-seekable devices, so we need + our own position. + */ + qint64 writePos; + /// Uncompressed size to write along with a raw file. + quint64 uncompressedSize; + /// CRC to write along with a raw file. + quint32 crc; + /// Whether \ref zip points to an internal QuaZip instance. + /** + This is true if the archive was opened by name, rather than by + supplying an existing QuaZip instance. + */ + bool internal; + /// The last error. + int zipError; + /// Resets \ref zipError. + inline void resetZipError() const {setZipError(UNZ_OK);} + /// Sets the zip error. + /** + This function is marked as const although it changes one field. + This allows to call it from const functions that don't change + anything by themselves. + */ + void setZipError(int zipError) const; + /// The constructor for the corresponding QuaZipFile constructor. + inline QuaZipFilePrivate(QuaZipFile *q): + q(q), + zip(NULL), + caseSensitivity(QuaZip::csDefault), + raw(false), + writePos(0), + uncompressedSize(0), + crc(0), + internal(true), + zipError(UNZ_OK) {} + /// The constructor for the corresponding QuaZipFile constructor. + inline QuaZipFilePrivate(QuaZipFile *q, const QString &zipName): + q(q), + caseSensitivity(QuaZip::csDefault), + raw(false), + writePos(0), + uncompressedSize(0), + crc(0), + internal(true), + zipError(UNZ_OK) + { + zip=new QuaZip(zipName); + } + /// The constructor for the corresponding QuaZipFile constructor. + inline QuaZipFilePrivate(QuaZipFile *q, const QString &zipName, const QString &fileName, + QuaZip::CaseSensitivity cs): + q(q), + raw(false), + writePos(0), + uncompressedSize(0), + crc(0), + internal(true), + zipError(UNZ_OK) + { + zip=new QuaZip(zipName); + this->fileName=fileName; + if (this->fileName.startsWith('/')) + this->fileName = this->fileName.mid(1); + this->caseSensitivity=cs; + } + /// The constructor for the QuaZipFile constructor accepting a file name. + inline QuaZipFilePrivate(QuaZipFile *q, QuaZip *zip): + q(q), + zip(zip), + raw(false), + writePos(0), + uncompressedSize(0), + crc(0), + internal(false), + zipError(UNZ_OK) {} + /// The destructor. + inline ~QuaZipFilePrivate() + { + if (internal) + delete zip; + } +}; + +QuaZipFile::QuaZipFile(): + p(new QuaZipFilePrivate(this)) +{ +} + +QuaZipFile::QuaZipFile(QObject *parent): + QIODevice(parent), + p(new QuaZipFilePrivate(this)) +{ +} + +QuaZipFile::QuaZipFile(const QString& zipName, QObject *parent): + QIODevice(parent), + p(new QuaZipFilePrivate(this, zipName)) +{ +} + +QuaZipFile::QuaZipFile(const QString& zipName, const QString& fileName, + QuaZip::CaseSensitivity cs, QObject *parent): + QIODevice(parent), + p(new QuaZipFilePrivate(this, zipName, fileName, cs)) +{ +} + +QuaZipFile::QuaZipFile(QuaZip *zip, QObject *parent): + QIODevice(parent), + p(new QuaZipFilePrivate(this, zip)) +{ +} + +QuaZipFile::~QuaZipFile() +{ + if (isOpen()) + close(); + delete p; +} + +QString QuaZipFile::getZipName() const +{ + return p->zip==NULL ? QString() : p->zip->getZipName(); +} + +QuaZip *QuaZipFile::getZip() const +{ + return p->internal ? NULL : p->zip; +} + +QString QuaZipFile::getActualFileName()const +{ + p->setZipError(UNZ_OK); + if (p->zip == NULL || (openMode() & WriteOnly)) + return QString(); + QString name=p->zip->getCurrentFileName(); + if(name.isNull()) + p->setZipError(p->zip->getZipError()); + return name; +} + +void QuaZipFile::setZipName(const QString& zipName) +{ + if(isOpen()) { + qWarning("QuaZipFile::setZipName(): file is already open - can not set ZIP name"); + return; + } + if(p->zip!=NULL && p->internal) + delete p->zip; + p->zip=new QuaZip(zipName); + p->internal=true; +} + +void QuaZipFile::setZip(QuaZip *zip) +{ + if(isOpen()) { + qWarning("QuaZipFile::setZip(): file is already open - can not set ZIP"); + return; + } + if(p->zip!=NULL && p->internal) + delete p->zip; + p->zip=zip; + p->fileName=QString(); + p->internal=false; +} + +void QuaZipFile::setFileName(const QString& fileName, QuaZip::CaseSensitivity cs) +{ + if(p->zip==NULL) { + qWarning("QuaZipFile::setFileName(): call setZipName() first"); + return; + } + if(!p->internal) { + qWarning("QuaZipFile::setFileName(): should not be used when not using internal QuaZip"); + return; + } + if(isOpen()) { + qWarning("QuaZipFile::setFileName(): can not set file name for already opened file"); + return; + } + p->fileName=fileName; + if (p->fileName.startsWith('/')) + p->fileName = p->fileName.mid(1); + p->caseSensitivity=cs; +} + +void QuaZipFilePrivate::setZipError(int zipError) const +{ + QuaZipFilePrivate *fakeThis = const_cast(this); // non-const + fakeThis->zipError=zipError; + if(zipError==UNZ_OK) + q->setErrorString(QString()); + else + q->setErrorString(QuaZipFile::tr("ZIP/UNZIP API error %1").arg(zipError)); +} + +bool QuaZipFile::open(OpenMode mode) +{ + return open(mode, NULL); +} + +bool QuaZipFile::open(OpenMode mode, int *method, int *level, bool raw, const char *password) +{ + p->resetZipError(); + if(isOpen()) { + qWarning("QuaZipFile::open(): already opened"); + return false; + } + if(mode&Unbuffered) { + qWarning("QuaZipFile::open(): Unbuffered mode is not supported"); + return false; + } + if((mode&ReadOnly)&&!(mode&WriteOnly)) { + if(p->internal) { + if(!p->zip->open(QuaZip::mdUnzip)) { + p->setZipError(p->zip->getZipError()); + return false; + } + if(!p->zip->setCurrentFile(p->fileName, p->caseSensitivity)) { + p->setZipError(p->zip->getZipError()); + p->zip->close(); + return false; + } + } else { + if(p->zip==NULL) { + qWarning("QuaZipFile::open(): zip is NULL"); + return false; + } + if(p->zip->getMode()!=QuaZip::mdUnzip) { + qWarning("QuaZipFile::open(): file open mode %d incompatible with ZIP open mode %d", + (int)mode, (int)p->zip->getMode()); + return false; + } + if(!p->zip->hasCurrentFile()) { + qWarning("QuaZipFile::open(): zip does not have current file"); + return false; + } + } + p->setZipError(unzOpenCurrentFile3(p->zip->getUnzFile(), method, level, (int)raw, password)); + if(p->zipError==UNZ_OK) { + setOpenMode(mode); + p->raw=raw; + return true; + } else + return false; + } + qWarning("QuaZipFile::open(): open mode %d not supported by this function", (int)mode); + return false; +} + +bool QuaZipFile::open(OpenMode mode, const QuaZipNewInfo& info, + const char *password, quint32 crc, + int method, int level, bool raw, + int windowBits, int memLevel, int strategy) +{ + zip_fileinfo info_z; + p->resetZipError(); + if(isOpen()) { + qWarning("QuaZipFile::open(): already opened"); + return false; + } + if((mode&WriteOnly)&&!(mode&ReadOnly)) { + if(p->internal) { + qWarning("QuaZipFile::open(): write mode is incompatible with internal QuaZip approach"); + return false; + } + if(p->zip==NULL) { + qWarning("QuaZipFile::open(): zip is NULL"); + return false; + } + if(p->zip->getMode()!=QuaZip::mdCreate&&p->zip->getMode()!=QuaZip::mdAppend&&p->zip->getMode()!=QuaZip::mdAdd) { + qWarning("QuaZipFile::open(): file open mode %d incompatible with ZIP open mode %d", + (int)mode, (int)p->zip->getMode()); + return false; + } + info_z.tmz_date.tm_year=info.dateTime.date().year(); + info_z.tmz_date.tm_mon=info.dateTime.date().month() - 1; + info_z.tmz_date.tm_mday=info.dateTime.date().day(); + info_z.tmz_date.tm_hour=info.dateTime.time().hour(); + info_z.tmz_date.tm_min=info.dateTime.time().minute(); + info_z.tmz_date.tm_sec=info.dateTime.time().second(); + info_z.dosDate = 0; + info_z.internal_fa=(uLong)info.internalAttr; + info_z.external_fa=(uLong)info.externalAttr; + if (p->zip->isDataDescriptorWritingEnabled()) + zipSetFlags(p->zip->getZipFile(), ZIP_WRITE_DATA_DESCRIPTOR); + else + zipClearFlags(p->zip->getZipFile(), ZIP_WRITE_DATA_DESCRIPTOR); + p->setZipError(zipOpenNewFileInZip3_64(p->zip->getZipFile(), + p->zip->getFileNameCodec()->fromUnicode(info.name).constData(), &info_z, + info.extraLocal.constData(), info.extraLocal.length(), + info.extraGlobal.constData(), info.extraGlobal.length(), + p->zip->getCommentCodec()->fromUnicode(info.comment).constData(), + method, level, (int)raw, + windowBits, memLevel, strategy, + password, (uLong)crc, p->zip->isZip64Enabled())); + if(p->zipError==UNZ_OK) { + p->writePos=0; + setOpenMode(mode); + p->raw=raw; + if(raw) { + p->crc=crc; + p->uncompressedSize=info.uncompressedSize; + } + return true; + } else + return false; + } + qWarning("QuaZipFile::open(): open mode %d not supported by this function", (int)mode); + return false; +} + +bool QuaZipFile::isSequential()const +{ + return true; +} + +qint64 QuaZipFile::pos()const +{ + if(p->zip==NULL) { + qWarning("QuaZipFile::pos(): call setZipName() or setZip() first"); + return -1; + } + if(!isOpen()) { + qWarning("QuaZipFile::pos(): file is not open"); + return -1; + } + if(openMode()&ReadOnly) + // QIODevice::pos() is broken for sequential devices, + // but thankfully bytesAvailable() returns the number of + // bytes buffered, so we know how far ahead we are. + return unztell64(p->zip->getUnzFile()) - QIODevice::bytesAvailable(); + else + return p->writePos; +} + +bool QuaZipFile::atEnd()const +{ + if(p->zip==NULL) { + qWarning("QuaZipFile::atEnd(): call setZipName() or setZip() first"); + return false; + } + if(!isOpen()) { + qWarning("QuaZipFile::atEnd(): file is not open"); + return false; + } + if(openMode()&ReadOnly) + // the same problem as with pos() + return QIODevice::bytesAvailable() == 0 + && unzeof(p->zip->getUnzFile())==1; + else + return true; +} + +qint64 QuaZipFile::size()const +{ + if(!isOpen()) { + qWarning("QuaZipFile::atEnd(): file is not open"); + return -1; + } + if(openMode()&ReadOnly) + return p->raw?csize():usize(); + else + return p->writePos; +} + +qint64 QuaZipFile::csize()const +{ + unz_file_info64 info_z; + p->setZipError(UNZ_OK); + if(p->zip==NULL||p->zip->getMode()!=QuaZip::mdUnzip) return -1; + p->setZipError(unzGetCurrentFileInfo64(p->zip->getUnzFile(), &info_z, NULL, 0, NULL, 0, NULL, 0)); + if(p->zipError!=UNZ_OK) + return -1; + return info_z.compressed_size; +} + +qint64 QuaZipFile::usize()const +{ + unz_file_info64 info_z; + p->setZipError(UNZ_OK); + if(p->zip==NULL||p->zip->getMode()!=QuaZip::mdUnzip) return -1; + p->setZipError(unzGetCurrentFileInfo64(p->zip->getUnzFile(), &info_z, NULL, 0, NULL, 0, NULL, 0)); + if(p->zipError!=UNZ_OK) + return -1; + return info_z.uncompressed_size; +} + +bool QuaZipFile::getFileInfo(QuaZipFileInfo *info) +{ + QuaZipFileInfo64 info64; + if (getFileInfo(&info64)) { + info64.toQuaZipFileInfo(*info); + return true; + } else { + return false; + } +} + +bool QuaZipFile::getFileInfo(QuaZipFileInfo64 *info) +{ + if(p->zip==NULL||p->zip->getMode()!=QuaZip::mdUnzip) return false; + p->zip->getCurrentFileInfo(info); + p->setZipError(p->zip->getZipError()); + return p->zipError==UNZ_OK; +} + +void QuaZipFile::close() +{ + p->resetZipError(); + if(p->zip==NULL||!p->zip->isOpen()) return; + if(!isOpen()) { + qWarning("QuaZipFile::close(): file isn't open"); + return; + } + if(openMode()&ReadOnly) + p->setZipError(unzCloseCurrentFile(p->zip->getUnzFile())); + else if(openMode()&WriteOnly) + if(isRaw()) p->setZipError(zipCloseFileInZipRaw64(p->zip->getZipFile(), p->uncompressedSize, p->crc)); + else p->setZipError(zipCloseFileInZip(p->zip->getZipFile())); + else { + qWarning("Wrong open mode: %d", (int)openMode()); + return; + } + if(p->zipError==UNZ_OK) setOpenMode(QIODevice::NotOpen); + else return; + if(p->internal) { + p->zip->close(); + p->setZipError(p->zip->getZipError()); + } +} + +qint64 QuaZipFile::readData(char *data, qint64 maxSize) +{ + p->setZipError(UNZ_OK); + qint64 bytesRead=unzReadCurrentFile(p->zip->getUnzFile(), data, (unsigned)maxSize); + if (bytesRead < 0) { + p->setZipError((int) bytesRead); + return -1; + } + return bytesRead; +} + +qint64 QuaZipFile::writeData(const char* data, qint64 maxSize) +{ + p->setZipError(ZIP_OK); + p->setZipError(zipWriteInFileInZip(p->zip->getZipFile(), data, (uint)maxSize)); + if(p->zipError!=ZIP_OK) return -1; + else { + p->writePos+=maxSize; + return maxSize; + } +} + +QString QuaZipFile::getFileName() const +{ + return p->fileName; +} + +QuaZip::CaseSensitivity QuaZipFile::getCaseSensitivity() const +{ + return p->caseSensitivity; +} + +bool QuaZipFile::isRaw() const +{ + return p->raw; +} + +int QuaZipFile::getZipError() const +{ + return p->zipError; +} + +qint64 QuaZipFile::bytesAvailable() const +{ + return size() - pos(); +} diff --git a/ultimmc/libraries/quazip/quazip/quazipfile.h b/ultimmc/libraries/quazip/quazip/quazipfile.h new file mode 100644 index 0000000..e27b7a4 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quazipfile.h @@ -0,0 +1,456 @@ +#ifndef QUA_ZIPFILE_H +#define QUA_ZIPFILE_H + +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant, see +quazip/(un)zip.h files for details, basically it's zlib license. + **/ + +#include + +#include "quazip_global.h" +#include "quazip.h" +#include "quazipnewinfo.h" + +class QuaZipFilePrivate; + +/// A file inside ZIP archive. +/** \class QuaZipFile quazipfile.h + * This is the most interesting class. Not only it provides C++ + * interface to the ZIP/UNZIP package, but also integrates it with Qt by + * subclassing QIODevice. This makes possible to access files inside ZIP + * archive using QTextStream or QDataStream, for example. Actually, this + * is the main purpose of the whole QuaZIP library. + * + * You can either use existing QuaZip instance to create instance of + * this class or pass ZIP archive file name to this class, in which case + * it will create internal QuaZip object. See constructors' descriptions + * for details. Writing is only possible with the existing instance. + * + * Note that due to the underlying library's limitation it is not + * possible to use multiple QuaZipFile instances to open several files + * in the same archive at the same time. If you need to write to + * multiple files in parallel, then you should write to temporary files + * first, then pack them all at once when you have finished writing. If + * you need to read multiple files inside the same archive in parallel, + * you should extract them all into a temporary directory first. + * + * \section quazipfile-sequential Sequential or random-access? + * + * At the first thought, QuaZipFile has fixed size, the start and the + * end and should be therefore considered random-access device. But + * there is one major obstacle to making it random-access: ZIP/UNZIP API + * does not support seek() operation and the only way to implement it is + * through reopening the file and re-reading to the required position, + * but this is prohibitively slow. + * + * Therefore, QuaZipFile is considered to be a sequential device. This + * has advantage of availability of the ungetChar() operation (QIODevice + * does not implement it properly for non-sequential devices unless they + * support seek()). Disadvantage is a somewhat strange behaviour of the + * size() and pos() functions. This should be kept in mind while using + * this class. + * + **/ +class QUAZIP_EXPORT QuaZipFile: public QIODevice { + friend class QuaZipFilePrivate; + Q_OBJECT + private: + QuaZipFilePrivate *p; + // these are not supported nor implemented + QuaZipFile(const QuaZipFile& that); + QuaZipFile& operator=(const QuaZipFile& that); + protected: + /// Implementation of the QIODevice::readData(). + qint64 readData(char *data, qint64 maxSize); + /// Implementation of the QIODevice::writeData(). + qint64 writeData(const char *data, qint64 maxSize); + public: + /// Constructs a QuaZipFile instance. + /** You should use setZipName() and setFileName() or setZip() before + * trying to call open() on the constructed object. + **/ + QuaZipFile(); + /// Constructs a QuaZipFile instance. + /** \a parent argument specifies this object's parent object. + * + * You should use setZipName() and setFileName() or setZip() before + * trying to call open() on the constructed object. + **/ + QuaZipFile(QObject *parent); + /// Constructs a QuaZipFile instance. + /** \a parent argument specifies this object's parent object and \a + * zipName specifies ZIP archive file name. + * + * You should use setFileName() before trying to call open() on the + * constructed object. + * + * QuaZipFile constructed by this constructor can be used for read + * only access. Use QuaZipFile(QuaZip*,QObject*) for writing. + **/ + QuaZipFile(const QString& zipName, QObject *parent =NULL); + /// Constructs a QuaZipFile instance. + /** \a parent argument specifies this object's parent object, \a + * zipName specifies ZIP archive file name and \a fileName and \a cs + * specify a name of the file to open inside archive. + * + * QuaZipFile constructed by this constructor can be used for read + * only access. Use QuaZipFile(QuaZip*,QObject*) for writing. + * + * \sa QuaZip::setCurrentFile() + **/ + QuaZipFile(const QString& zipName, const QString& fileName, + QuaZip::CaseSensitivity cs =QuaZip::csDefault, QObject *parent =NULL); + /// Constructs a QuaZipFile instance. + /** \a parent argument specifies this object's parent object. + * + * \a zip is the pointer to the existing QuaZip object. This + * QuaZipFile object then can be used to read current file in the + * \a zip or to write to the file inside it. + * + * \warning Using this constructor for reading current file can be + * tricky. Let's take the following example: + * \code + * QuaZip zip("archive.zip"); + * zip.open(QuaZip::mdUnzip); + * zip.setCurrentFile("file-in-archive"); + * QuaZipFile file(&zip); + * file.open(QIODevice::ReadOnly); + * // ok, now we can read from the file + * file.read(somewhere, some); + * zip.setCurrentFile("another-file-in-archive"); // oops... + * QuaZipFile anotherFile(&zip); + * anotherFile.open(QIODevice::ReadOnly); + * anotherFile.read(somewhere, some); // this is still ok... + * file.read(somewhere, some); // and this is NOT + * \endcode + * So, what exactly happens here? When we change current file in the + * \c zip archive, \c file that references it becomes invalid + * (actually, as far as I understand ZIP/UNZIP sources, it becomes + * closed, but QuaZipFile has no means to detect it). + * + * Summary: do not close \c zip object or change its current file as + * long as QuaZipFile is open. Even better - use another constructors + * which create internal QuaZip instances, one per object, and + * therefore do not cause unnecessary trouble. This constructor may + * be useful, though, if you already have a QuaZip instance and do + * not want to access several files at once. Good example: + * \code + * QuaZip zip("archive.zip"); + * zip.open(QuaZip::mdUnzip); + * // first, we need some information about archive itself + * QByteArray comment=zip.getComment(); + * // and now we are going to access files inside it + * QuaZipFile file(&zip); + * for(bool more=zip.goToFirstFile(); more; more=zip.goToNextFile()) { + * file.open(QIODevice::ReadOnly); + * // do something cool with file here + * file.close(); // do not forget to close! + * } + * zip.close(); + * \endcode + **/ + QuaZipFile(QuaZip *zip, QObject *parent =NULL); + /// Destroys a QuaZipFile instance. + /** Closes file if open, destructs internal QuaZip object (if it + * exists and \em is internal, of course). + **/ + virtual ~QuaZipFile(); + /// Returns the ZIP archive file name. + /** If this object was created by passing QuaZip pointer to the + * constructor, this function will return that QuaZip's file name + * (or null string if that object does not have file name yet). + * + * Otherwise, returns associated ZIP archive file name or null + * string if there are no name set yet. + * + * \sa setZipName() getFileName() + **/ + QString getZipName()const; + /// Returns a pointer to the associated QuaZip object. + /** Returns \c NULL if there is no associated QuaZip or it is + * internal (so you will not mess with it). + **/ + QuaZip* getZip()const; + /// Returns file name. + /** This function returns file name you passed to this object either + * by using + * QuaZipFile(const QString&,const QString&,QuaZip::CaseSensitivity,QObject*) + * or by calling setFileName(). Real name of the file may differ in + * case if you used case-insensitivity. + * + * Returns null string if there is no file name set yet. This is the + * case when this QuaZipFile operates on the existing QuaZip object + * (constructor QuaZipFile(QuaZip*,QObject*) or setZip() was used). + * + * \sa getActualFileName + **/ + QString getFileName() const; + /// Returns case sensitivity of the file name. + /** This function returns case sensitivity argument you passed to + * this object either by using + * QuaZipFile(const QString&,const QString&,QuaZip::CaseSensitivity,QObject*) + * or by calling setFileName(). + * + * Returns unpredictable value if getFileName() returns null string + * (this is the case when you did not used setFileName() or + * constructor above). + * + * \sa getFileName + **/ + QuaZip::CaseSensitivity getCaseSensitivity() const; + /// Returns the actual file name in the archive. + /** This is \em not a ZIP archive file name, but a name of file inside + * archive. It is not necessary the same name that you have passed + * to the + * QuaZipFile(const QString&,const QString&,QuaZip::CaseSensitivity,QObject*), + * setFileName() or QuaZip::setCurrentFile() - this is the real file + * name inside archive, so it may differ in case if the file name + * search was case-insensitive. + * + * Equivalent to calling getCurrentFileName() on the associated + * QuaZip object. Returns null string if there is no associated + * QuaZip object or if it does not have a current file yet. And this + * is the case if you called setFileName() but did not open the + * file yet. So this is perfectly fine: + * \code + * QuaZipFile file("somezip.zip"); + * file.setFileName("somefile"); + * QString name=file.getName(); // name=="somefile" + * QString actual=file.getActualFileName(); // actual is null string + * file.open(QIODevice::ReadOnly); + * QString actual=file.getActualFileName(); // actual can be "SoMeFiLe" on Windows + * \endcode + * + * \sa getZipName(), getFileName(), QuaZip::CaseSensitivity + **/ + QString getActualFileName()const; + /// Sets the ZIP archive file name. + /** Automatically creates internal QuaZip object and destroys + * previously created internal QuaZip object, if any. + * + * Will do nothing if this file is already open. You must close() it + * first. + **/ + void setZipName(const QString& zipName); + /// Returns \c true if the file was opened in raw mode. + /** If the file is not open, the returned value is undefined. + * + * \sa open(OpenMode,int*,int*,bool,const char*) + **/ + bool isRaw() const; + /// Binds to the existing QuaZip instance. + /** This function destroys internal QuaZip object, if any, and makes + * this QuaZipFile to use current file in the \a zip object for any + * further operations. See QuaZipFile(QuaZip*,QObject*) for the + * possible pitfalls. + * + * Will do nothing if the file is currently open. You must close() + * it first. + **/ + void setZip(QuaZip *zip); + /// Sets the file name. + /** Will do nothing if at least one of the following conditions is + * met: + * - ZIP name has not been set yet (getZipName() returns null + * string). + * - This QuaZipFile is associated with external QuaZip. In this + * case you should call that QuaZip's setCurrentFile() function + * instead! + * - File is already open so setting the name is meaningless. + * + * \sa QuaZip::setCurrentFile + **/ + void setFileName(const QString& fileName, QuaZip::CaseSensitivity cs =QuaZip::csDefault); + /// Opens a file for reading. + /** Returns \c true on success, \c false otherwise. + * Call getZipError() to get error code. + * + * \note Since ZIP/UNZIP API provides buffered reading only, + * QuaZipFile does not support unbuffered reading. So do not pass + * QIODevice::Unbuffered flag in \a mode, or open will fail. + **/ + virtual bool open(OpenMode mode); + /// Opens a file for reading. + /** \overload + * Argument \a password specifies a password to decrypt the file. If + * it is NULL then this function behaves just like open(OpenMode). + **/ + inline bool open(OpenMode mode, const char *password) + {return open(mode, NULL, NULL, false, password);} + /// Opens a file for reading. + /** \overload + * Argument \a password specifies a password to decrypt the file. + * + * An integers pointed by \a method and \a level will receive codes + * of the compression method and level used. See unzip.h. + * + * If raw is \c true then no decompression is performed. + * + * \a method should not be \c NULL. \a level can be \c NULL if you + * don't want to know the compression level. + **/ + bool open(OpenMode mode, int *method, int *level, bool raw, const char *password =NULL); + /// Opens a file for writing. + /** \a info argument specifies information about file. It should at + * least specify a correct file name. Also, it is a good idea to + * specify correct timestamp (by default, current time will be + * used). See QuaZipNewInfo. + * + * The \a password argument specifies the password for crypting. Pass NULL + * if you don't need any crypting. The \a crc argument was supposed + * to be used for crypting too, but then it turned out that it's + * false information, so you need to set it to 0 unless you want to + * use the raw mode (see below). + * + * Arguments \a method and \a level specify compression method and + * level. The only method supported is Z_DEFLATED, but you may also + * specify 0 for no compression. If all of the files in the archive + * use both method 0 and either level 0 is explicitly specified or + * data descriptor writing is disabled with + * QuaZip::setDataDescriptorWritingEnabled(), then the + * resulting archive is supposed to be compatible with the 1.0 ZIP + * format version, should you need that. Except for this, \a level + * has no other effects with method 0. + * + * If \a raw is \c true, no compression is performed. In this case, + * \a crc and uncompressedSize field of the \a info are required. + * + * Arguments \a windowBits, \a memLevel, \a strategy provide zlib + * algorithms tuning. See deflateInit2() in zlib. + **/ + bool open(OpenMode mode, const QuaZipNewInfo& info, + const char *password =NULL, quint32 crc =0, + int method =Z_DEFLATED, int level =Z_DEFAULT_COMPRESSION, bool raw =false, + int windowBits =-MAX_WBITS, int memLevel =DEF_MEM_LEVEL, int strategy =Z_DEFAULT_STRATEGY); + /// Returns \c true, but \ref quazipfile-sequential "beware"! + virtual bool isSequential()const; + /// Returns current position in the file. + /** Implementation of the QIODevice::pos(). When reading, this + * function is a wrapper to the ZIP/UNZIP unztell(), therefore it is + * unable to keep track of the ungetChar() calls (which is + * non-virtual and therefore is dangerous to reimplement). So if you + * are using ungetChar() feature of the QIODevice, this function + * reports incorrect value until you get back characters which you + * ungot. + * + * When writing, pos() returns number of bytes already written + * (uncompressed unless you use raw mode). + * + * \note Although + * \ref quazipfile-sequential "QuaZipFile is a sequential device" + * and therefore pos() should always return zero, it does not, + * because it would be misguiding. Keep this in mind. + * + * This function returns -1 if the file or archive is not open. + * + * Error code returned by getZipError() is not affected by this + * function call. + **/ + virtual qint64 pos()const; + /// Returns \c true if the end of file was reached. + /** This function returns \c false in the case of error. This means + * that you called this function on either not open file, or a file + * in the not open archive or even on a QuaZipFile instance that + * does not even have QuaZip instance associated. Do not do that + * because there is no means to determine whether \c false is + * returned because of error or because end of file was reached. + * Well, on the other side you may interpret \c false return value + * as "there is no file open to check for end of file and there is + * no end of file therefore". + * + * When writing, this function always returns \c true (because you + * are always writing to the end of file). + * + * Error code returned by getZipError() is not affected by this + * function call. + **/ + virtual bool atEnd()const; + /// Returns file size. + /** This function returns csize() if the file is open for reading in + * raw mode, usize() if it is open for reading in normal mode and + * pos() if it is open for writing. + * + * Returns -1 on error, call getZipError() to get error code. + * + * \note This function returns file size despite that + * \ref quazipfile-sequential "QuaZipFile is considered to be sequential device", + * for which size() should return bytesAvailable() instead. But its + * name would be very misguiding otherwise, so just keep in mind + * this inconsistence. + **/ + virtual qint64 size()const; + /// Returns compressed file size. + /** Equivalent to calling getFileInfo() and then getting + * compressedSize field, but more convenient and faster. + * + * File must be open for reading before calling this function. + * + * Returns -1 on error, call getZipError() to get error code. + **/ + qint64 csize()const; + /// Returns uncompressed file size. + /** Equivalent to calling getFileInfo() and then getting + * uncompressedSize field, but more convenient and faster. See + * getFileInfo() for a warning. + * + * File must be open for reading before calling this function. + * + * Returns -1 on error, call getZipError() to get error code. + **/ + qint64 usize()const; + /// Gets information about current file. + /** This function does the same thing as calling + * QuaZip::getCurrentFileInfo() on the associated QuaZip object, + * but you can not call getCurrentFileInfo() if the associated + * QuaZip is internal (because you do not have access to it), while + * you still can call this function in that case. + * + * File must be open for reading before calling this function. + * + * \return \c false in the case of an error. + * + * This function doesn't support zip64, but will still work fine on zip64 + * archives if file sizes are below 4 GB, otherwise the values will be set + * as if converted using QuaZipFileInfo64::toQuaZipFileInfo(). + * + * \sa getFileInfo(QuaZipFileInfo64*) + **/ + bool getFileInfo(QuaZipFileInfo *info); + /// Gets information about current file with zip64 support. + /** + * @overload + * + * \sa getFileInfo(QuaZipFileInfo*) + */ + bool getFileInfo(QuaZipFileInfo64 *info); + /// Closes the file. + /** Call getZipError() to determine if the close was successful. + **/ + virtual void close(); + /// Returns the error code returned by the last ZIP/UNZIP API call. + int getZipError() const; + /// Returns the number of bytes available for reading. + virtual qint64 bytesAvailable() const; +}; + +#endif diff --git a/ultimmc/libraries/quazip/quazip/quazipfileinfo.cpp b/ultimmc/libraries/quazip/quazip/quazipfileinfo.cpp new file mode 100644 index 0000000..f11c910 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quazipfileinfo.cpp @@ -0,0 +1,176 @@ +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include "quazipfileinfo.h" + +static QFile::Permissions permissionsFromExternalAttr(quint32 externalAttr) { + quint32 uPerm = (externalAttr & 0xFFFF0000u) >> 16; + QFile::Permissions perm = 0; + if ((uPerm & 0400) != 0) + perm |= QFile::ReadOwner; + if ((uPerm & 0200) != 0) + perm |= QFile::WriteOwner; + if ((uPerm & 0100) != 0) + perm |= QFile::ExeOwner; + if ((uPerm & 0040) != 0) + perm |= QFile::ReadGroup; + if ((uPerm & 0020) != 0) + perm |= QFile::WriteGroup; + if ((uPerm & 0010) != 0) + perm |= QFile::ExeGroup; + if ((uPerm & 0004) != 0) + perm |= QFile::ReadOther; + if ((uPerm & 0002) != 0) + perm |= QFile::WriteOther; + if ((uPerm & 0001) != 0) + perm |= QFile::ExeOther; + return perm; + +} + +QFile::Permissions QuaZipFileInfo::getPermissions() const +{ + return permissionsFromExternalAttr(externalAttr); +} + +QFile::Permissions QuaZipFileInfo64::getPermissions() const +{ + return permissionsFromExternalAttr(externalAttr); +} + +bool QuaZipFileInfo64::toQuaZipFileInfo(QuaZipFileInfo &info) const +{ + bool noOverflow = true; + info.name = name; + info.versionCreated = versionCreated; + info.versionNeeded = versionNeeded; + info.flags = flags; + info.method = method; + info.dateTime = dateTime; + info.crc = crc; + if (compressedSize > 0xFFFFFFFFu) { + info.compressedSize = 0xFFFFFFFFu; + noOverflow = false; + } else { + info.compressedSize = compressedSize; + } + if (uncompressedSize > 0xFFFFFFFFu) { + info.uncompressedSize = 0xFFFFFFFFu; + noOverflow = false; + } else { + info.uncompressedSize = uncompressedSize; + } + info.diskNumberStart = diskNumberStart; + info.internalAttr = internalAttr; + info.externalAttr = externalAttr; + info.comment = comment; + info.extra = extra; + return noOverflow; +} + +static QDateTime getNTFSTime(const QByteArray &extra, int position, + int *fineTicks) +{ + QDateTime dateTime; + for (int i = 0; i <= extra.size() - 4; ) { + unsigned type = static_cast(static_cast( + extra.at(i))) + | (static_cast(static_cast( + extra.at(i + 1))) << 8); + i += 2; + unsigned length = static_cast(static_cast( + extra.at(i))) + | (static_cast(static_cast( + extra.at(i + 1))) << 8); + i += 2; + if (type == QUAZIP_EXTRA_NTFS_MAGIC && length >= 32) { + i += 4; // reserved + while (i <= extra.size() - 4) { + unsigned tag = static_cast( + static_cast(extra.at(i))) + | (static_cast( + static_cast(extra.at(i + 1))) + << 8); + i += 2; + int tagsize = static_cast( + static_cast(extra.at(i))) + | (static_cast( + static_cast(extra.at(i + 1))) + << 8); + i += 2; + if (tag == QUAZIP_EXTRA_NTFS_TIME_MAGIC + && tagsize >= position + 8) { + i += position; + quint64 mtime = static_cast( + static_cast(extra.at(i))) + | (static_cast(static_cast( + extra.at(i + 1))) << 8) + | (static_cast(static_cast( + extra.at(i + 2))) << 16) + | (static_cast(static_cast( + extra.at(i + 3))) << 24) + | (static_cast(static_cast( + extra.at(i + 4))) << 32) + | (static_cast(static_cast( + extra.at(i + 5))) << 40) + | (static_cast(static_cast( + extra.at(i + 6))) << 48) + | (static_cast(static_cast( + extra.at(i + 7))) << 56); + // the NTFS time is measured from 1601 for whatever reason + QDateTime base(QDate(1601, 1, 1), QTime(0, 0), Qt::UTC); + dateTime = base.addMSecs(mtime / 10000); + if (fineTicks != NULL) { + *fineTicks = static_cast(mtime % 10000); + } + i += tagsize - position; + } else { + i += tagsize; + } + + } + } else { + i += length; + } + } + if (fineTicks != NULL && dateTime.isNull()) { + *fineTicks = 0; + } + return dateTime; +} + +QDateTime QuaZipFileInfo64::getNTFSmTime(int *fineTicks) const +{ + return getNTFSTime(extra, 0, fineTicks); +} + +QDateTime QuaZipFileInfo64::getNTFSaTime(int *fineTicks) const +{ + return getNTFSTime(extra, 8, fineTicks); +} + +QDateTime QuaZipFileInfo64::getNTFScTime(int *fineTicks) const +{ + return getNTFSTime(extra, 16, fineTicks); +} diff --git a/ultimmc/libraries/quazip/quazip/quazipfileinfo.h b/ultimmc/libraries/quazip/quazip/quazipfileinfo.h new file mode 100644 index 0000000..4e142a4 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quazipfileinfo.h @@ -0,0 +1,178 @@ +#ifndef QUA_ZIPFILEINFO_H +#define QUA_ZIPFILEINFO_H + +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include +#include +#include + +#include "quazip_global.h" + +/// Information about a file inside archive. +/** + * \deprecated Use QuaZipFileInfo64 instead. Not only it supports large files, + * but also more convenience methods as well. + * + * Call QuaZip::getCurrentFileInfo() or QuaZipFile::getFileInfo() to + * fill this structure. */ +struct QUAZIP_EXPORT QuaZipFileInfo { + /// File name. + QString name; + /// Version created by. + quint16 versionCreated; + /// Version needed to extract. + quint16 versionNeeded; + /// General purpose flags. + quint16 flags; + /// Compression method. + quint16 method; + /// Last modification date and time. + QDateTime dateTime; + /// CRC. + quint32 crc; + /// Compressed file size. + quint32 compressedSize; + /// Uncompressed file size. + quint32 uncompressedSize; + /// Disk number start. + quint16 diskNumberStart; + /// Internal file attributes. + quint16 internalAttr; + /// External file attributes. + quint32 externalAttr; + /// Comment. + QString comment; + /// Extra field. + QByteArray extra; + /// Get the file permissions. + /** + Returns the high 16 bits of external attributes converted to + QFile::Permissions. + */ + QFile::Permissions getPermissions() const; +}; + +/// Information about a file inside archive (with zip64 support). +/** Call QuaZip::getCurrentFileInfo() or QuaZipFile::getFileInfo() to + * fill this structure. */ +struct QUAZIP_EXPORT QuaZipFileInfo64 { + /// File name. + QString name; + /// Version created by. + quint16 versionCreated; + /// Version needed to extract. + quint16 versionNeeded; + /// General purpose flags. + quint16 flags; + /// Compression method. + quint16 method; + /// Last modification date and time. + /** + * This is the time stored in the standard ZIP header. This format only allows + * to store time with 2-second precision, so the seconds will always be even + * and the milliseconds will always be zero. If you need more precise + * date and time, you can try to call the getNTFSmTime() function or + * its siblings, provided that the archive itself contains these NTFS times. + */ + QDateTime dateTime; + /// CRC. + quint32 crc; + /// Compressed file size. + quint64 compressedSize; + /// Uncompressed file size. + quint64 uncompressedSize; + /// Disk number start. + quint16 diskNumberStart; + /// Internal file attributes. + quint16 internalAttr; + /// External file attributes. + quint32 externalAttr; + /// Comment. + QString comment; + /// Extra field. + QByteArray extra; + /// Get the file permissions. + /** + Returns the high 16 bits of external attributes converted to + QFile::Permissions. + */ + QFile::Permissions getPermissions() const; + /// Converts to QuaZipFileInfo + /** + If any of the fields are greater than 0xFFFFFFFFu, they are set to + 0xFFFFFFFFu exactly, not just truncated. This function should be mainly used + for compatibility with the old code expecting QuaZipFileInfo, in the cases + when it's impossible or otherwise unadvisable (due to ABI compatibility + reasons, for example) to modify that old code to use QuaZipFileInfo64. + + \return \c true if all fields converted correctly, \c false if an overflow + occured. + */ + bool toQuaZipFileInfo(QuaZipFileInfo &info) const; + /// Returns the NTFS modification time + /** + * The getNTFS*Time() functions only work if there is an NTFS extra field + * present. Otherwise, they all return invalid null timestamps. + * @param fineTicks If not NULL, the fractional part of milliseconds returned + * there, measured in 100-nanosecond ticks. Will be set to + * zero if there is no NTFS extra field. + * @sa dateTime + * @sa getNTFSaTime() + * @sa getNTFScTime() + * @return The NTFS modification time, UTC + */ + QDateTime getNTFSmTime(int *fineTicks = NULL) const; + /// Returns the NTFS access time + /** + * The getNTFS*Time() functions only work if there is an NTFS extra field + * present. Otherwise, they all return invalid null timestamps. + * @param fineTicks If not NULL, the fractional part of milliseconds returned + * there, measured in 100-nanosecond ticks. Will be set to + * zero if there is no NTFS extra field. + * @sa dateTime + * @sa getNTFSmTime() + * @sa getNTFScTime() + * @return The NTFS access time, UTC + */ + QDateTime getNTFSaTime(int *fineTicks = NULL) const; + /// Returns the NTFS creation time + /** + * The getNTFS*Time() functions only work if there is an NTFS extra field + * present. Otherwise, they all return invalid null timestamps. + * @param fineTicks If not NULL, the fractional part of milliseconds returned + * there, measured in 100-nanosecond ticks. Will be set to + * zero if there is no NTFS extra field. + * @sa dateTime + * @sa getNTFSmTime() + * @sa getNTFSaTime() + * @return The NTFS creation time, UTC + */ + QDateTime getNTFScTime(int *fineTicks = NULL) const; + /// Checks whether the file is encrypted. + bool isEncrypted() const {return (flags & 1) != 0;} +}; + +#endif diff --git a/ultimmc/libraries/quazip/quazip/quazipnewinfo.cpp b/ultimmc/libraries/quazip/quazip/quazipnewinfo.cpp new file mode 100644 index 0000000..8015d92 --- /dev/null +++ b/ultimmc/libraries/quazip/quazip/quazipnewinfo.cpp @@ -0,0 +1,286 @@ +/* +Copyright (C) 2005-2014 Sergey A. Tachenov + +This file is part of QuaZIP. + +QuaZIP is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 2.1 of the License, or +(at your option) any later version. + +QuaZIP is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with QuaZIP. If not, see . + +See COPYING file for the full LGPL text. + +Original ZIP package is copyrighted by Gilles Vollant and contributors, +see quazip/(un)zip.h files for details. Basically it's the zlib license. +*/ + +#include + +#include "quazipnewinfo.h" + +#include + +static void QuaZipNewInfo_setPermissions(QuaZipNewInfo *info, + QFile::Permissions perm, bool isDir, bool isSymLink = false) +{ + quint32 uPerm = isDir ? 0040000 : 0100000; + + if ( isSymLink ) { +#ifdef Q_OS_WIN + uPerm = 0200000; +#else + uPerm = 0120000; +#endif + } + + if ((perm & QFile::ReadOwner) != 0) + uPerm |= 0400; + if ((perm & QFile::WriteOwner) != 0) + uPerm |= 0200; + if ((perm & QFile::ExeOwner) != 0) + uPerm |= 0100; + if ((perm & QFile::ReadGroup) != 0) + uPerm |= 0040; + if ((perm & QFile::WriteGroup) != 0) + uPerm |= 0020; + if ((perm & QFile::ExeGroup) != 0) + uPerm |= 0010; + if ((perm & QFile::ReadOther) != 0) + uPerm |= 0004; + if ((perm & QFile::WriteOther) != 0) + uPerm |= 0002; + if ((perm & QFile::ExeOther) != 0) + uPerm |= 0001; + info->externalAttr = (info->externalAttr & ~0xFFFF0000u) | (uPerm << 16); +} + +template +void QuaZipNewInfo_init(QuaZipNewInfo &self, const FileInfo &existing) +{ + self.name = existing.name; + self.dateTime = existing.dateTime; + self.internalAttr = existing.internalAttr; + self.externalAttr = existing.externalAttr; + self.comment = existing.comment; + self.extraLocal = existing.extra; + self.extraGlobal = existing.extra; + self.uncompressedSize = existing.uncompressedSize; +} + +QuaZipNewInfo::QuaZipNewInfo(const QuaZipFileInfo &existing) +{ + QuaZipNewInfo_init(*this, existing); +} + +QuaZipNewInfo::QuaZipNewInfo(const QuaZipFileInfo64 &existing) +{ + QuaZipNewInfo_init(*this, existing); +} + +QuaZipNewInfo::QuaZipNewInfo(const QString& name): + name(name), dateTime(QDateTime::currentDateTime()), internalAttr(0), externalAttr(0), + uncompressedSize(0) +{ +} + +QuaZipNewInfo::QuaZipNewInfo(const QString& name, const QString& file): + name(name), internalAttr(0), externalAttr(0), uncompressedSize(0) +{ + QFileInfo info(file); + QDateTime lm = info.lastModified(); + if (!info.exists()) { + dateTime = QDateTime::currentDateTime(); + } else { + dateTime = lm; + QuaZipNewInfo_setPermissions(this, info.permissions(), info.isDir(), info.isSymLink()); + } +} + +void QuaZipNewInfo::setFileDateTime(const QString& file) +{ + QFileInfo info(file); + QDateTime lm = info.lastModified(); + if (info.exists()) + dateTime = lm; +} + +void QuaZipNewInfo::setFilePermissions(const QString &file) +{ + QFileInfo info = QFileInfo(file); + QFile::Permissions perm = info.permissions(); + QuaZipNewInfo_setPermissions(this, perm, info.isDir(), info.isSymLink()); +} + +void QuaZipNewInfo::setPermissions(QFile::Permissions permissions) +{ + QuaZipNewInfo_setPermissions(this, permissions, name.endsWith('/')); +} + +void QuaZipNewInfo::setFileNTFSTimes(const QString &fileName) +{ + QFileInfo fi(fileName); + if (!fi.exists()) { + qWarning("QuaZipNewInfo::setFileNTFSTimes(): '%s' doesn't exist", + fileName.toUtf8().constData()); + return; + } + setFileNTFSmTime(fi.lastModified()); + setFileNTFSaTime(fi.lastRead()); + setFileNTFScTime(fi.created()); +} + +static void setNTFSTime(QByteArray &extra, const QDateTime &time, int position, + int fineTicks) { + int ntfsPos = -1, timesPos = -1; + unsigned ntfsLength = 0, ntfsTimesLength = 0; + for (int i = 0; i <= extra.size() - 4; ) { + unsigned type = static_cast(static_cast( + extra.at(i))) + | (static_cast(static_cast( + extra.at(i + 1))) << 8); + i += 2; + unsigned length = static_cast(static_cast( + extra.at(i))) + | (static_cast(static_cast( + extra.at(i + 1))) << 8); + i += 2; + if (type == QUAZIP_EXTRA_NTFS_MAGIC) { + ntfsPos = i - 4; // the beginning of the NTFS record + ntfsLength = length; + if (length <= 4) { + break; // no times in the NTFS record + } + i += 4; // reserved + while (i <= extra.size() - 4) { + unsigned tag = static_cast( + static_cast(extra.at(i))) + | (static_cast( + static_cast(extra.at(i + 1))) + << 8); + i += 2; + unsigned tagsize = static_cast( + static_cast(extra.at(i))) + | (static_cast( + static_cast(extra.at(i + 1))) + << 8); + i += 2; + if (tag == QUAZIP_EXTRA_NTFS_TIME_MAGIC) { + timesPos = i - 4; // the beginning of the NTFS times tag + ntfsTimesLength = tagsize; + break; + } else { + i += tagsize; + } + } + break; // I ain't going to search for yet another NTFS record! + } else { + i += length; + } + } + if (ntfsPos == -1) { + // No NTFS record, need to create one. + ntfsPos = extra.size(); + ntfsLength = 32; + extra.resize(extra.size() + 4 + ntfsLength); + // the NTFS record header + extra[ntfsPos] = static_cast(QUAZIP_EXTRA_NTFS_MAGIC); + extra[ntfsPos + 1] = static_cast(QUAZIP_EXTRA_NTFS_MAGIC >> 8); + extra[ntfsPos + 2] = 32; // the 2-byte size in LittleEndian + extra[ntfsPos + 3] = 0; + // zero the record + memset(extra.data() + ntfsPos + 4, 0, 32); + timesPos = ntfsPos + 8; + // now set the tag data + extra[timesPos] = static_cast(QUAZIP_EXTRA_NTFS_TIME_MAGIC); + extra[timesPos + 1] = static_cast(QUAZIP_EXTRA_NTFS_TIME_MAGIC + >> 8); + // the size: + extra[timesPos + 2] = 24; + extra[timesPos + 3] = 0; + ntfsTimesLength = 24; + } + if (timesPos == -1) { + // No time tag in the NTFS record, need to add one. + timesPos = ntfsPos + 4 + ntfsLength; + extra.resize(extra.size() + 28); + // Now we need to move the rest of the field + // (possibly zero bytes, but memmove() is OK with that). + // 0 ......... ntfsPos .. ntfsPos + 4 ... timesPos + //
    + memmove(extra.data() + timesPos + 28, extra.data() + timesPos, + extra.size() - 28 - timesPos); + ntfsLength += 28; + // now set the tag data + extra[timesPos] = static_cast(QUAZIP_EXTRA_NTFS_TIME_MAGIC); + extra[timesPos + 1] = static_cast(QUAZIP_EXTRA_NTFS_TIME_MAGIC + >> 8); + // the size: + extra[timesPos + 2] = 24; + extra[timesPos + 3] = 0; + // zero the record + memset(extra.data() + timesPos + 4, 0, 24); + ntfsTimesLength = 24; + } + if (ntfsTimesLength < 24) { + // Broken times field. OK, this is really unlikely, but just in case... + size_t timesEnd = timesPos + 4 + ntfsTimesLength; + extra.resize(extra.size() + (24 - ntfsTimesLength)); + // Move it! + // 0 ......... timesPos .... timesPos + 4 .. timesEnd + //